diff options
| author | christos <christos@NetBSD.org> | 2000-09-04 21:47:02 +0000 |
|---|---|---|
| committer | christos <christos@NetBSD.org> | 2000-09-04 21:47:02 +0000 |
| commit | 5c984f3d998e2c70cb63be34997f0bbec88ccf3e (patch) | |
| tree | b1eba87db109d0fcc25e992d3a467a01e80e6909 /gnu | |
| parent | 7803a4e4eff3e1fcfd26156a9c448fae2e3ef3c3 (diff) | |
Initial import of cvs-1.10.8 from ftp.cvshome.org minus the os support.
Diffstat (limited to 'gnu')
83 files changed, 75113 insertions, 0 deletions
diff --git a/gnu/dist/cvs/contrib/.cvsignore b/gnu/dist/cvs/contrib/.cvsignore new file mode 100644 index 00000000000..deb5efc5de7 --- /dev/null +++ b/gnu/dist/cvs/contrib/.cvsignore @@ -0,0 +1,14 @@ +Makefile +clmerge +cln_hist +commit_prep +cvs2vendor +cvs_acls +cvscheck +log +log_accum +mfpipe +rcs-to-cvs +rcs2log +rcslock +sccs2rcs diff --git a/gnu/dist/cvs/contrib/ChangeLog b/gnu/dist/cvs/contrib/ChangeLog new file mode 100644 index 00000000000..46cf007051b --- /dev/null +++ b/gnu/dist/cvs/contrib/ChangeLog @@ -0,0 +1,406 @@ +1999-01-19 Graham Stoney <greyham@research.canon.com.au> + + * log.pl: The author commited the canonical perl "localtime" Y2K + offence, of printing "19$year" instead of (1900 + $year). Of + course, the result is non-compliance in year 2000. Fix it. + +1998-10-14 Jim Kingdon + + * ccvs-rsh.pl: Removed; it was not in DISTFILES so it didn't + actually get distributed. I'm going to move it to the web on the + theory that the web is a better place for such things. + * README: Don't mention it. + + * Makefile.in (dist-dir, distclean): Remove references to elib. + * elib: Remove this subdirectory and all its contents. It went + with pcl-cvs, which is no longer distributed with CVS. + +1998-09-22 Jim Kingdon <kingdon@harvey.cyclic.com> + + * pvcs_to_rcs: Removed; it was not in DISTFILES so it didn't + actually get distributed. I'm going to move it to the web on the + theory that the web is a better place for such things. + * README: Don't mention it. + +1998-09-10 Jim Kingdon + + Check in Paul Eggert <eggert@twinsun.com>'s submission of + 1998-08-15. I also ran "cvs admin -ko" on this file so that his + version number would be intact (not an ideal solution, because + people will import it into other repositories, but I don't feel + like hacking the master version). + * rcs2log.sh: Sync with master version at gnu.org. + +1998-08-15 Jim Kingdon <kingdon@harvey.cyclic.com> + + * README: Don't mention listener, since it was removed a while + ago. + * listen2.c, listen2.mak: Removed; because there is no easy way to + pass a socket (as opposed to file descriptor) from one process to + another on Windows, this isn't a promising approach (at least not + in this form). + * Makefile.in (DISTFILES): Remove them. + * .cvsignore: Remove listen2.ncb listen2.mdp Debug. + +1998-05-11 W. Bradley Rubenstein + + * log.pl: Check for errors from open and exec. + +Sat Feb 21 21:59:45 1998 Ian Lance Taylor <ian@cygnus.com> + + * Makefile.in (clean): Change "/bin/rm" to "rm". + +Thu Aug 7 22:42:23 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * pvcs_to_rcs: Remove RCS keywords. Remove $Log and move the data + to this ChangeLog (below). Add paragraph that David Martin + emailed along with the script. + + Revision 1.6 1997/03/07 16:21:28 divad + Need to explicitly state archive name in PVCS get command for + those cases where the case of the workfile and the case of the + archive file are different (OS/2) + + Revision 1.5 1997/03/07 00:31:04 divad + Added capitalized extensions and framemaker files as binaries; + also overriding any path specification for workfiles at PVCS + checkout (most annoying). + + Revision 1.4 1997/03/06 21:04:55 divad + Added \n to the end of each comment line to prevent multi-line + comments for a single revision from "merging" + + Revision 1.3 1997/03/06 19:50:25 divad + Corrected bug in binary extensions; correcting processing + comment strings with double quotes + + Revision 1.2 1997/03/06 17:29:10 divad + Provided list of extensions (rather than using Unix file + command) to determine which files are binary; also printing + version label as they are applied + + Revision 1.1 1997/02/26 00:04:29 divad + Perl script to convert pvcs archives to rcs archives + + * README: mention pvcs_to_rcs. + * pvcs_to_rcs: New file. This is the file as I got it from David + Martin. Will be checking in the tweaks shortly. + +17 May 1997 Jim Kingdon + + * listen2.c: Failed attempt at making this do what it was + intended to do. Will need to rethink the approach. + * listen2.mak: The usual involuntary tweaks. + * .cvsignore: Add listen2.ncb listen2.mdp. + +Mon May 12 11:59:23 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * listener.c: Removed; see ../ChangeLog for rationale. + +10 May 1997 Jim Kingdon + + * listen2.c, listen2.mak: New files. + * Makefile.in (DISTFILES): Add them. + * .cvsignore: Add Debug. + +Thu Feb 20 22:43:45 1997 David J MacKenzie <djm@va.pubnix.com> + + * rcs-to-cvs.sh: Put temporary files in /var/tmp or /usr/tmp + whichever one exists. Just call "vi" not "/usr/ucb/vi". + +Mon Feb 17 08:51:37 1997 Greg A. Woods <woods@most.weird.com> + + * .cvsignore: added 'cvs2vendor' target from Feb. 12 changes. + + * log_accum.pl (build_header): added "Repository:" to the report + header to show the first argument supplied to the script by CVS. + [[this value seems spuriously to be wrong when client is used]] + ($hostdomain): correct order of initialization from the Feb. 12 + changes. + ($modulename): add more commentary about using '-M' to to get a + meaningful string here. + Tweak a few other comments from the Feb. 12 changes. + +Wed Feb 12 10:27:48 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cln_hist.pl, commit_prep.pl, cvs2vendor.sh, cvs_acls.pl, + cvscheck.man, cvscheck.sh, cvshelp.man, descend.man, descend.sh, + log_accum.pl, mfpipe.pl, rcs-to-cvs.sh, rcs2log.sh, rcs2sccs.sh, + sccs2rcs.csh: Remove $Id; we decided to get rid of these some + time ago. + +Wed Feb 12 00:24:33 1997 Greg A. Woods <woods@most.weird.com> + + * cvs2vendor.sh: new script. + * README: noted new cvs2vendor script. + * Makefile.in (DISTFILES): added cvs2vendor.sh. + (CONTRIB_PROGS): added cvs2vendor. + + * log_accum.pl (show_wd): new variable, initialized to 0. + - set $show_wd if '-w' option found while parsing @ARGV. + - don't add 'In directory' line to report header unless $show_wd + is set. + (domainname): prepend a leading '.' if none there so that + concatenation with $hostname works (those with a FQDN hostname + *and* a domainname still lose). + (mail_notification): don't set a "From:" header -- the mailer will. + +Wed Jan 8 14:48:58 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in, README, log.pl: Remove CVSid; we decided to get rid + of these some time ago. + +Thu Jan 2 13:30:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in: Remove "675" paragraph; see ../ChangeLog for rationale. + +Thu Oct 17 18:28:25 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * patch-2.1-.new-fix: Removed; it was not in DISTFILES so it never + made it into distributions. It also isn't clear what it has to do + with CVS. It is available from + ftp://ftp.weird.com/pub/patch-2.1-.new-fix + * README: Remove entry for patch-2.1-.new-fix. + +Wed Oct 16 10:22:44 1996 Jim Blandy <jimb@totoro.cyclic.com> + + * rcs2log.sh: Change date output format to something CVS 1.9 + accepts. I think this breaks the Sep 29 change, but I don't have + a copy of CVS 1.5 handy, so I can't find a format that works with + both, and I think it's more important that it work with the + version it's distributed with. + +Sat Oct 12 21:18:19 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * README: Don't mention pcl-cvs; it isn't here any more. + +Sun Sep 29 19:45:19 1996 Greg A. Woods <woods@most.weird.com> + + * README: add entry for patch-2.1-.new-fix. + + * README: re-write the top section a bit. + + * patch-2.1-.new-fix: re-generated using fixed "cvs patch" command. + + * patch-2.1-.new-fix: new file. + +Sun Sep 29 14:25:28 1996 Dave Love <d.love@dl.ac.uk> + + * rcs2log.sh (month_data): Make default date format acceptable to + CVS post v1.8 as well as earlier CVSs and RCS. + Message-Id: <199609291546.QAA25531@mserv1.dl.ac.uk> + To: bug-gnu-emacs@prep.ai.mit.edu + +Thu Aug 29 11:58:03 1996 Jim Blandy <jimb@totoro.cyclic.com> + + * rcs2log: Update FSF address. + + * rcs2log: Be more aggressive about finding the author's full + name; try nismatch and ypmatch. + + * rcs2log: If the hostname appears not to be fully qualified, see + if domainname provides any useful information. + +Fri Aug 16 16:02:36 1996 Norbert Kiesel <nk@col.sw-ley.de> + + * Makefile.in (installdirs): support this target + +Mon May 6 13:04:57 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (install): Don't tell user to run cvsinit. It isn't + called cvsinit anymore, and it isn't necessary (repositories are, + and need to be, compatible between cvs versions). + +Sun Apr 14 11:30:36 1996 Karl Fogel <kfogel@floss.red-bean.com> + + * Removed pcl-cvs/ subdir; see tools/ subdir in the top-level from + now on. + Added elib/ subdir. + + * Makefile.in (dist-dir): Removed all references to pcl-cvs/ + subdir. + +Wed Mar 6 10:20:28 1996 Greg A. Woods <woods@most.weird.com> + + * log_accum.pl: ($MAILER): use sendmail directly to allow other + headers to be included + * log_accum.pl (mail_notification): add support to allow settting + of Reply-To and Date header fields in the sent mail; remove $mailto + argument and use the global variable (as with $replyto). + * log_accum.pl: add -R option for mail_notification()'s optional + Reply-To value [default to $login] + +Fri Mar 1 01:51:56 1996 Benjamin J. Lee <benjamin@cyclic.com> + + * listener.c: added as mentioned in ../README.VMS + +Mon Feb 19 13:37:36 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * README: Don't just tell people "we don't want your script"; tell + them what to do instead. + +Thu Feb 1 14:28:16 1996 Karl Fogel <kfogel@floss.red-bean.com> + + * Makefile.in (DISTFILES): added `rcs2sccs.sh', as mentioned in + README. + +Thu Jan 18 09:39:16 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * README: Talk about submitting changes to contrib directory. + +Tue Nov 14 15:28:25 1995 Greg A. Woods <woods@most.weird.com> + + * README: fix some spelling and other typos + + * Makefile.in: if I need reminding to run cvsinit.... + +Tue Nov 14 13:47:40 1995 Greg A. Woods <woods@most.weird.com> + + * log_accum.pl: + - Fix 'cvs status' to use global -Qq options + - fix up a couple of comments, incl., my proper address + + * log.pl: add a CVSid and fix a couple of comments + +Sun Oct 1 02:02:57 1995 Peter Wemm <peter@haywire.dialix.com> + + * Makefile.in: supply a suffix rule to deal with .sh "source" + +Sat Jul 29 17:29:13 1995 James Kingdon <kingdon@harvey.cyclic.com> + + * log.pl: Use global options -Qq, not command options -Qq. + + * Makefile.in (install): Look for $(PROGS) and + $(CONTRIB_PROGS) in build dir, not srcdir. + +Fri Jul 28 19:48:45 1995 Paul Eggert <eggert@twinsun.com> + + * rcs2log.sh: Sync with latest Emacs snapshot. + +Thu Jul 27 20:29:30 1995 Jim Blandy <jimb@totoro.cyclic.com> + + * rcs2log.sh: import of initial WNT port work + +Fri Jul 14 22:38:44 1995 Jim Blandy <jimb@totoro.cyclic.com> + + * rcs-to-cvs.sh: Changes from David J. Mackenzie. + Set permissions on new repository files correctly. + Ignore *~ files. + +Thu Jul 13 23:04:12 CDT 1995 Jim Meyering (meyering@comco.com) + + * Makefile.in (.pl, .csh): *Never* redirect output directly to + the target (usu $@) of a rule. Instead, redirect to a temporary + file, and then move that temporary to the target. I chose to + name temporary files $@-t. Remember to be careful that the length + of the temporary file name not exceed the 14-character limit. + +Sun Jul 9 21:16:53 1995 Karl Fogel <kfogel@floss.cyclic.com> + + These are actually Greg Woods' changes: + + * clmerge.pl, cvscheck.sh, descend.sh, dirfns.shar, rcs-to-cvs.sh, + rcs2log.sh, sccs2rcs.csh: renamed from the corresponding files + sans extensions. + + * rcs2sccs.sh: new file. + +Sun Jul 9 19:03:00 1995 Greg A. Woods <woods@most.weird.com> + + * rcs2log.sh: oops, one more thing that should not have been + there. + - fix interpreter file syntax. + - remove "fix" for separating filenames and comments + + * Makefile.in: hmm... thought rcs2log was in RCS-5.7 for some + reason -- it's not, so we'll install it from here.... + - fix typo -- that's what you get for re-doing changes by hand! + - updates to support proper transformation and installation of + renamed files (from previous local changes) + + * .cvsignore: one more target noted... + + * sccs2rcs.csh: set up the interpreter file for updating by + Makefile (from previous local changes) + + * log_accum.pl, log.pl, commit_prep.pl: + - set up the interpreter file for updating by Makefile + - various modifications, updates, and enhancements + (from previous local changes) + + * rcslock.pl, mfpipe.pl, cvs_acls.pl, cln_hist.pl, clmerge.pl: + - set up the interpreter file for updating by Makefile + (from previous local changes) + - include changes from 1.5 here too, if any + + * README: + - remove extensions from filenames to match installed names + (from previous local changes) + + * .cvsignore: - added $(CONTRIB_PROGS) (from previous local changes) + + +Thu Jun 29 10:43:07 1995 James Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (distclean): Also remove pcl-cvs/Makefile. + +Thu Jun 8 15:32:29 1995 Jim Kingdon (kingdon@lioth.cygnus.com) + + * intro.doc: Added. + * Makefile.in (DISTFILES): Add intro.doc. + +Sat May 27 08:46:00 1995 Jim Meyering (meyering@comco.com) + + * Makefile.in (Makefile): Regenerate only Makefile in current + directory when Makefile.in is out of date. Depend on ../config.status. + +Mon May 8 13:06:29 1995 Bryan O'Sullivan <bos@serpentine.com> + + * README: added an entry for ccvs-rsh.pl. + +Sun Apr 30 23:50:32 1995 Bryan O'Sullivan <bos@serpentine.com> + + * ccvs-rsh.pl: fixed a typo and added more flexible use of + CVS_PROXY_USER. + +Sun Apr 30 14:56:21 1995 Jim Blandy <jimb@totoro.bio.indiana.edu> + + * clmerge: Changes from Tom Tromey --- fix bug in date comparison + function. + +Sat Apr 29 20:53:08 1995 Bryan O'Sullivan <bos@serpentine.com> + + * ccvs-rsh.pl: created. See the file itself for documentation. + + * Makefile.in (DISTFILES): added ccvs-rsh.pl to the list of + files to install. + +Fri Apr 28 22:32:45 1995 Jim Blandy <jimb@totoro.bio.indiana.edu> + + * Makefile.in (DISTFILES): Brought up-to-date with current + directory contents. + (dist-dir): Renamed from dist-dir; use DISTDIR variable, passed + from parent. + +Mon Feb 13 13:32:07 1995 Jim Blandy <jimb@totoro.bio.indiana.edu> + + * rcs2log: rcs2log was originally in this tree; how did it get + deleted? Anyway, this is the version distributed with Emacs + 19.28, hacked to support CVS and Remote CVS. + +Mon Jul 26 13:18:23 1993 David J. Mackenzie (djm@thepub.cygnus.com) + + * rcs-to-cvs: Rewrite in sh. + +Wed Jul 14 21:16:40 1993 David J. Mackenzie (djm@thepub.cygnus.com) + + * rcs-to-cvs: Don't source .cshrc or hardcode paths. + Make respository dir if needed. Don't suppress errors + (such as prompts) from co. + +Wed Feb 26 18:04:40 1992 K. Richard Pixley (rich@cygnus.com) + + * Makefile.in, configure.in: removed traces of namesubdir, + -subdirs, $(subdir), $(unsubdir), some rcs triggers. Forced + copyrights to '92, changed some from Cygnus to FSF. + diff --git a/gnu/dist/cvs/contrib/Makefile.in b/gnu/dist/cvs/contrib/Makefile.in new file mode 100644 index 00000000000..1c56339042f --- /dev/null +++ b/gnu/dist/cvs/contrib/Makefile.in @@ -0,0 +1,127 @@ +# Makefile for GNU CVS contributed sources. +# Do not use this makefile directly, but only from `../Makefile'. +# Copyright (C) 1986, 1988-1990 Free Software Foundation, Inc. + +# 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, 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. + +SHELL = /bin/sh + +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +VPATH = @srcdir@ + +prefix = @prefix@ +exec_prefix = @exec_prefix@ + +# Where to install the executables. +bindir = $(exec_prefix)/bin + +# Where to put the system-wide .cvsrc file +libdir = $(prefix)/lib + +# Where to put the manual pages. +mandir = $(prefix)/man + +# where to find command interpreters +perl_path = @perl_path@ +csh_path = @csh_path@ + +# Use cp if you don't have install. +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ + +DISTFILES = \ + ChangeLog README .cvsignore intro.doc \ + Makefile.in clmerge.pl cln_hist.pl commit_prep.pl cvs2vendor.sh \ + cvs_acls.pl cvscheck.sh cvscheck.man cvshelp.man descend.sh \ + descend.man dirfns.shar log.pl log_accum.pl mfpipe.pl rcs-to-cvs.sh \ + rcs2log.sh rcslock.pl sccs2rcs.csh rcs2sccs.sh + +# files installed in $(libdir)/cvs/contrib +# +CONTRIB_FILES = README intro.doc cvscheck.man + +# things we actually build and install.... +# +PROGS = rcs2log +CONTRIB_PROGS = clmerge cln_hist commit_prep cvs2vendor cvs_acls cvscheck \ + log log_accum mfpipe rcs-to-cvs rcs2log rcslock sccs2rcs + +.SUFFIXES: .pl .sh .csh + +.pl: + rm -f $@ + sed -e 's,xPERL_PATHx,$(perl_path),' $< > $@-t + mv $@-t $@ + chmod +x $@ + +.csh: + rm -f $@ + sed -e 's,xCSH_PATHx,$(csh_path),' $< > $@-t + mv $@-t $@ + chmod +x $@ + +.sh: + rm -f $@ + cp $< $@ + chmod +x $@ + +all: Makefile $(PROGS) $(CONTRIB_PROGS) +.PHONY: all + +install: all installdirs + for f in $(CONTRIB_FILES) ; do\ + $(INSTALL_DATA) $(srcdir)/$$f $(libdir)/cvs/contrib/$$f; \ + done + for f in $(CONTRIB_PROGS) ; do\ + $(INSTALL_PROGRAM) $$f $(libdir)/cvs/contrib/$$f; \ + done + for f in $(PROGS) ; do\ + $(INSTALL_PROGRAM) $$f $(bindir)/$$f; \ + done +.PHONY: install + +installdirs: + $(top_srcdir)/mkinstalldirs $(libdir)/cvs/contrib +.PHONY: installdirs + +tags: +.PHONY: tags + +TAGS: +.PHONY: TAGS + +ls: + @echo $(DISTFILES) +.PHONY: ls + +clean: + rm -f *.o core +.PHONY: clean + +distclean: clean + rm -f Makefile $(PROGS) $(CONTRIB_PROGS) +.PHONY: distclean + +realclean: distclean +.PHONY: realclean + +dist-dir: + mkdir ${DISTDIR} + for i in ${DISTFILES}; do \ + ln $(srcdir)/$${i} ${DISTDIR}; \ + done +.PHONY: dist-dir + +subdir = contrib +Makefile: ../config.status Makefile.in + cd .. && CONFIG_FILES=$(subdir)/$@ CONFIG_HEADERS= ./config.status diff --git a/gnu/dist/cvs/contrib/README b/gnu/dist/cvs/contrib/README new file mode 100644 index 00000000000..8a93f8f689b --- /dev/null +++ b/gnu/dist/cvs/contrib/README @@ -0,0 +1,104 @@ +This "contrib" directory is a place holder for code/scripts sent to me +by contributors around the world. This README file will be kept +up-to-date from release to release. BUT, we must point out that these +contributions are really, REALLY UNSUPPORTED. In fact, we probably +don't even know what some of them really do. We certainly do not +guarantee to have tried them, or ported them to work with this CVS +distribution. If you have questions, your best bet is to contact the +original author, but you should not necessarily expect a reply, since +the author may not be available at the address given. + +USE AT YOUR OWN RISK -- and all that stuff. + +"Unsupported" also means that no one has volunteered to accept and check +in changes to this directory. So submissions for new scripts to add +here are unlikely to be accepted. Suggested changes to the existing +scripts here conceivably might, but that isn't clear either, unless of +course they come from the original author of the script. + +If you have some software that works with CVS that you wish to offer it +is suggested that you make it available by FTP or HTTP and then announce +it on the info-cvs mailing list. + +There is a web page of software related to CVS at the following URL which +would presumably be willing to list your software. + + http://www.loria.fr/~molli/cvs-index.html + +An attempt at a table of Contents for this directory: + + README This file. + log A perl script suitable for including in your + $CVSROOT/CVSROOT/loginfo file for logging commit + changes. Includes the RCS revision of the change + as part of the log. + Contributed by Kevin Samborn <samborn@sunrise.com>. + commit_prep A perl script, to be combined with log_accum.pl, to + log_accum provide for a way to combine the individual log + messages of a multi-directory "commit" into a + single log message, and mail the result somewhere. + Can also do other checks for $Id and that you are + committing the correct revision of the file. + Read the comments carefully. + Contributed by David Hampton <hampton@cisco.com>. + mfpipe Another perl script for logging. Allows you to + pipe the log message to a file and/or send mail + to some alias. + Contributed by John Clyne <clyne@niwot.scd.ucar.edu>. + rcs-to-cvs Script to import sources that may have been under + RCS control already. + Contributed by Per Cederqvist <ceder@lysator.liu.se>. + cvscheck Identifies files added, changed, or removed in a + cvscheck.man checked out CVS tree; also notices unknown files. + Contributed by Lowell Skoog <fluke!lowell@uunet.uu.net> + cvshelp.man An introductory manual page written by Lowell Skoog + <fluke!lowell@uunet.uu.net>. It is most likely + out-of-date relative to CVS 1.3, but still may be + useful. + dirfns A shar file which contains some code that might + help your system support opendir/readdir/closedir, + if it does not already. + Copied from the C-News distribution. + rcslock A perl script that can be added to your commitinfo + file that tries to determine if your RCS file is + currently locked by someone else, as might be the + case for a binary file. + Contributed by John Rouillard <rouilj@cs.umb.edu>. + cvs_acls A perl script that implements Access Control Lists + by using the "commitinfo" hook provided with the + "cvs commit" command. + Contributed by David G. Grubbs <dgg@ksr.com>. + descend A shell script that can be used to recursively + descend.man descend through a directory. In CVS 1.2, this was + very useful, since many of the commands were not + recursive. In CVS 1.3 (and later), however, most of + the commands are recursive. However, this may still + come in handy. + Contributed by Lowell Skoog <fluke!lowell@uunet.uu.net> + cln_hist A perl script to compress your + $CVSROOT/CVSROOT/history file, as it can grow quite + large after extended use. + Contributed by David G. Grubbs <dgg@ksr.com> + sccs2rcs A C-shell script that can convert (some) SCCS files + into RCS files, retaining the info contained in the + SCCS file (like dates, author, and log message). + Contributed by Ken Cox <kenstir@viewlogic.com>. + intro.doc A user's view of what you need to know to get + started with CVS. + Contributed by <Steven.Pemberton@cwi.nl>. + rcs2sccs A shell script to convert simple RCS files into + SCCS files, originally gleaned off the network + somewhere (originally by "kenc") and modified by + Jerry Jelinek <jerry@rmtc.Central.Sun.COM> and + Brian Berliner <berliner@sun.com> to increase + robustness and add support for one-level of branches. + rcs2log A shell script to create a ChangeLog-format file + given only a set of RCS files. + Contributed by Paul Eggert <eggert@twinsun.com>. + clmerge A perl script to handle merge conflicts in GNU + style ChangeLog files . + Contributed by Tom Tromey <tromey@busco.lanl.gov>. + cvs2vendor A shell script to move changes from a repository + that was started without a vendor branch to one + that has a vendor branch. + Contributed by Greg A. Woods <woods@planix.com> diff --git a/gnu/dist/cvs/contrib/clmerge.pl b/gnu/dist/cvs/contrib/clmerge.pl new file mode 100644 index 00000000000..ac813713a82 --- /dev/null +++ b/gnu/dist/cvs/contrib/clmerge.pl @@ -0,0 +1,152 @@ +#! xPERL_PATHx + +# Merge conflicted ChangeLogs +# tromey Mon Aug 15 1994 + +# Usage is: +# +# cl-merge [-i] file ... +# +# With -i, it works in place (backups put in a ~ file). Otherwise the +# merged ChangeLog is printed to stdout. + +# Please report any bugs to me. I wrote this yesterday, so there are no +# guarantees about its performance. I recommend checking its output +# carefully. If you do send a bug report, please include the failing +# ChangeLog, so I can include it in my test suite. +# +# Tom +# --- +# tromey@busco.lanl.gov Member, League for Programming Freedom +# Sadism and farce are always inexplicably linked. +# -- Alexander Theroux + + +# Month->number mapping. Used for sorting. +%months = ('Jan', 0, + 'Feb', 1, + 'Mar', 2, + 'Apr', 3, + 'May', 4, + 'Jun', 5, + 'Jul', 6, + 'Aug', 7, + 'Sep', 8, + 'Oct', 9, + 'Nov', 10, + 'Dec', 11); + +# If '-i' is given, do it in-place. +if ($ARGV[0] eq '-i') { + shift (@ARGV); + $^I = '~'; +} + +$lastkey = ''; +$lastval = ''; +$conf = 0; +%conflist = (); + +$tjd = 0; + +# Simple state machine. The states: +# +# 0 Not in conflict. Just copy input to output. +# 1 Beginning an entry. Next non-blank line is key. +# 2 In entry. Entry beginner transitions to state 1. +while (<>) { + if (/^<<<</ || /^====/) { + # Start of a conflict. + + # Copy last key into array. + if ($lastkey ne '') { + $conflist{$lastkey} = $lastval; + + $lastkey = ''; + $lastval = ''; + } + + $conf = 1; + } elsif (/^>>>>/) { + # End of conflict. Output. + + # Copy last key into array. + if ($lastkey ne '') { + $conflist{$lastkey} = $lastval; + + $lastkey = ''; + $lastval = ''; + } + + foreach (reverse sort clcmp keys %conflist) { + print STDERR "doing $_" if $tjd; + print $_; + print $conflist{$_}; + } + + $lastkey = ''; + $lastval = ''; + $conf = 0; + %conflist = (); + } elsif ($conf == 1) { + # Beginning an entry. Skip empty lines. Error if not a real + # beginner. + if (/^$/) { + # Empty line; just skip at this point. + } elsif (/^[MTWFS]/) { + # Looks like the name of a day; assume opener and move to + # "in entry" state. + $lastkey = $_; + $conf = 2; + print STDERR "found $_" if $tjd; + } else { + die ("conflict crosses entry boundaries: $_"); + } + } elsif ($conf == 2) { + # In entry. Copy into variable until we see beginner line. + if (/^[MTWFS]/) { + # Entry beginner line. + + # Copy last key into array. + if ($lastkey ne '') { + $conflist{$lastkey} = $lastval; + + $lastkey = ''; + $lastval = ''; + } + + $lastkey = $_; + print STDERR "found $_" if $tjd; + $lastval = ''; + } else { + $lastval .= $_; + } + } else { + # Just copy. + print; + } +} + +# Compare ChangeLog time strings like <=>. +# +# 0 1 2 3 +# Thu Aug 11 13:22:42 1994 Tom Tromey (tromey@creche.colorado.edu) +# 0123456789012345678901234567890 +# +sub clcmp { + # First check year. + $r = substr ($a, 20, 4) <=> substr ($b, 20, 4); + + # Now check month. + $r = $months{substr ($a, 4, 3)} <=> $months{substr ($b, 4, 3)} if !$r; + + # Now check day. + $r = substr ($a, 8, 2) <=> substr ($b, 8, 2) if !$r; + + # Now check time (3 parts). + $r = substr ($a, 11, 2) <=> substr ($b, 11, 2) if !$r; + $r = substr ($a, 14, 2) <=> substr ($b, 14, 2) if !$r; + $r = substr ($a, 17, 2) <=> substr ($b, 17, 2) if !$r; + + $r; +} diff --git a/gnu/dist/cvs/contrib/cln_hist.pl b/gnu/dist/cvs/contrib/cln_hist.pl new file mode 100644 index 00000000000..19a0b815b78 --- /dev/null +++ b/gnu/dist/cvs/contrib/cln_hist.pl @@ -0,0 +1,91 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# Contributed by David G. Grubbs <dgg@ksr.com> +# +# Clean up the history file. 10 Record types: MAR OFT WUCG +# +# WUCG records are thrown out. +# MAR records are retained. +# T records: retain only last tag with same combined tag/module. +# +# Two passes: Walk through the first time and remember the +# 1. Last Tag record with same "tag" and "module" names. +# 2. Last O record with unique user/module/directory, unless followed +# by a matching F record. +# + +$r = $ENV{"CVSROOT"}; +$c = "$r/CVSROOT"; +$h = "$c/history"; + +eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';" + while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV)); +exit 255 if $die; # process any variable=value switches + +%tags = (); +%outs = (); + +# +# Move history file to safe place and re-initialize a new one. +# +rename($h, "$h.bak"); +open(XX, ">$h"); +close(XX); + +# +# Pass1 -- remember last tag and checkout. +# +open(HIST, "$h.bak"); +while (<HIST>) { + next if /^[MARWUCG]/; + + # Save whole line keyed by tag|module + if (/^T/) { + @tmp = split(/\|/, $_); + $tags{$tmp[4] . '|' . $tmp[5]} = $_; + } + # Save whole line + if (/^[OF]/) { + @tmp = split(/\|/, $_); + $outs{$tmp[1] . '|' . $tmp[2] . '|' . $tmp[5]} = $_; + } +} + +# +# Pass2 -- print out what we want to save. +# +open(SAVE, ">$h.work"); +open(HIST, "$h.bak"); +while (<HIST>) { + next if /^[FWUCG]/; + + # If whole line matches saved (i.e. "last") one, print it. + if (/^T/) { + @tmp = split(/\|/, $_); + next if $tags{$tmp[4] . '|' . $tmp[5]} ne $_; + } + # Save whole line + if (/^O/) { + @tmp = split(/\|/, $_); + next if $outs{$tmp[1] . '|' . $tmp[2] . '|' . $tmp[5]} ne $_; + } + + print SAVE $_; +} + +# +# Put back the saved stuff +# +system "cat $h >> $h.work"; + +if (-s $h) { + rename ($h, "$h.interim"); + print "history.interim has non-zero size.\n"; +} else { + unlink($h); +} + +rename ("$h.work", $h); + +exit(0); diff --git a/gnu/dist/cvs/contrib/commit_prep.pl b/gnu/dist/cvs/contrib/commit_prep.pl new file mode 100644 index 00000000000..bf0ce92f22f --- /dev/null +++ b/gnu/dist/cvs/contrib/commit_prep.pl @@ -0,0 +1,215 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# +# Perl filter to handle pre-commit checking of files. This program +# records the last directory where commits will be taking place for +# use by the log_accum.pl script. For new files, it forces the +# existence of a RCS "Id" keyword in the first ten lines of the file. +# For existing files, it checks version number in the "Id" line to +# prevent losing changes because an old version of a file was copied +# into the direcory. +# +# Possible future enhancements: +# +# Check for cruft left by unresolved conflicts. Search for +# "^<<<<<<<$", "^-------$", and "^>>>>>>>$". +# +# Look for a copyright and automagically update it to the +# current year. [[ bad idea! -- woods ]] +# +# +# Contributed by David Hampton <hampton@cisco.com> +# +# Hacked on lots by Greg A. Woods <woods@web.net> + +# +# Configurable options +# + +# Constants (remember to protect strings from RCS keyword substitution) +# +$LAST_FILE = "/tmp/#cvs.lastdir"; # must match name in log_accum.pl +$ENTRIES = "CVS/Entries"; + +# Patterns to find $Log keywords in files +# +$LogString1 = "\\\$\\Log: .* \\\$"; +$LogString2 = "\\\$\\Log\\\$"; +$NoLog = "%s - contains an RCS \$Log keyword. It must not!\n"; + +# pattern to match an RCS Id keyword line with an existing ID +# +$IDstring = "\"@\\(#\\)[^:]*:.*\\\$\Id: .*\\\$\""; +$NoId = " +%s - Does not contain a properly formatted line with the keyword \"Id:\". + I.e. no lines match \"" . $IDstring . "\". + Please see the template files for an example.\n"; + +# pattern to match an RCS Id keyword line for a new file (i.e. un-expanded) +# +$NewId = "\"@(#)[^:]*:.*\\$\Id\\$\""; + +$NoName = " +%s - The ID line should contain only \"@(#)module/path:\$Name\$:\$\Id\$\" + for a newly created file.\n"; + +$BadName = " +%s - The file name '%s' in the ID line does not match + the actual filename.\n"; + +$BadVersion = " +%s - How dare you!!! You replaced your copy of the file '%s', + which was based upon version %s, with an %s version based + upon %s. Please move your '%s' out of the way, perform an + update to get the current version, and them merge your changes + into that file, then try the commit again.\n"; + +# +# Subroutines +# + +sub write_line { + local($filename, $line) = @_; + open(FILE, ">$filename") || die("Cannot open $filename, stopped"); + print(FILE $line, "\n"); + close(FILE); +} + +sub check_version { + local($i, $id, $rname, $version); + local($filename, $cvsversion) = @_; + + open(FILE, "<$filename") || return(0); + + @all_lines = (); + $idpos = -1; + $newidpos = -1; + for ($i = 0; <FILE>; $i++) { + chop; + push(@all_lines, $_); + if ($_ =~ /$IDstring/) { + $idpos = $i; + } + if ($_ =~ /$NewId/) { + $newidpos = $i; + } + } + + if (grep(/$LogString1/, @all_lines) || grep(/$LogString2/, @all_lines)) { + print STDERR sprintf($NoLog, $filename); + return(1); + } + + if ($debug != 0) { + print STDERR sprintf("file = %s, version = %d.\n", $filename, $cvsversion{$filename}); + } + + if ($cvsversion{$filename} == 0) { + if ($newidpos != -1 && $all_lines[$newidpos] !~ /$NewId/) { + print STDERR sprintf($NoName, $filename); + return(1); + } + return(0); + } + + if ($idpos == -1) { + print STDERR sprintf($NoId, $filename); + return(1); + } + + $line = $all_lines[$idpos]; + $pos = index($line, "Id: "); + if ($debug != 0) { + print STDERR sprintf("%d in '%s'.\n", $pos, $line); + } + ($id, $rname, $version) = split(' ', substr($line, $pos)); + if ($rname ne "$filename,v") { + print STDERR sprintf($BadName, $filename, substr($rname, 0, length($rname)-2)); + return(1); + } + if ($cvsversion{$filename} < $version) { + print STDERR sprintf($BadVersion, $filename, $filename, $cvsversion{$filename}, + "newer", $version, $filename); + return(1); + } + if ($cvsversion{$filename} > $version) { + print STDERR sprintf($BadVersion, $filename, $filename, $cvsversion{$filename}, + "older", $version, $filename); + return(1); + } + return(0); +} + +# +# Main Body +# + +$id = getpgrp(); # You *must* use a shell that does setpgrp()! + +# Check each file (except dot files) for an RCS "Id" keyword. +# +$check_id = 0; + +# Record the directory for later use by the log_accumulate stript. +# +$record_directory = 0; + +# parse command line arguments +# +while (@ARGV) { + $arg = shift @ARGV; + + if ($arg eq '-d') { + $debug = 1; + print STDERR "Debug turned on...\n"; + } elsif ($arg eq '-c') { + $check_id = 1; + } elsif ($arg eq '-r') { + $record_directory = 1; + } else { + push(@files, $arg); + } +} + +$directory = shift @files; + +if ($debug != 0) { + print STDERR "dir - ", $directory, "\n"; + print STDERR "files - ", join(":", @files), "\n"; + print STDERR "id - ", $id, "\n"; +} + +# Suck in the CVS/Entries file +# +open(ENTRIES, $ENTRIES) || die("Cannot open $ENTRIES.\n"); +while (<ENTRIES>) { + local($filename, $version) = split('/', substr($_, 1)); + $cvsversion{$filename} = $version; +} + +# Now check each file name passed in, except for dot files. Dot files +# are considered to be administrative files by this script. +# +if ($check_id != 0) { + $failed = 0; + foreach $arg (@files) { + if (index($arg, ".") == 0) { + next; + } + $failed += &check_version($arg); + } + if ($failed) { + print STDERR "\n"; + exit(1); + } +} + +# Record this directory as the last one checked. This will be used +# by the log_accumulate script to determine when it is processing +# the final directory of a multi-directory commit. +# +if ($record_directory != 0) { + &write_line("$LAST_FILE.$id", $directory); +} +exit(0); diff --git a/gnu/dist/cvs/contrib/cvs2vendor.sh b/gnu/dist/cvs/contrib/cvs2vendor.sh new file mode 100644 index 00000000000..234f4d95e5d --- /dev/null +++ b/gnu/dist/cvs/contrib/cvs2vendor.sh @@ -0,0 +1,142 @@ +#! /bin/sh +# +# cvs2vendor - move revsisions from files in A to files in B +# +# The primary reason for this script is to move deltas from a +# non-vendor branched repository onto a fresh vendor branched one, +# skipping the initial checkin in assumption that it is the same in +# both repositories. This way you can take a project that was moved +# into CVS without the benefit of the vendor branch and for all +# intents and purposes add the vendor branch underneath the existing +# deltas. +# +# This script is also a decent example of repository maintenance using +# raw RCS commands (if I do say so myself! ;-). +# +# Tags are preserved. +# +# The timestamp of the initial vendor branch revision will be adjusted +# to be the same as the 1.1 revision of each source file. +# +# Extra branches in the source directory will cause breakage. +# +# Intermediate files are created in the current working directory +# where this script is started. +# +# Written by Greg A. Woods <woods@planix.com>, based on rcs2sccs +# (retains some of the rlog parsing from it). +# +# The copyright is in the Public Domain. +# + +if [ $# -ne 2 ]; then + echo USAGE: $0 srcdir dstdir + exit 2 +fi +tsrcdir=$1 +tdstdir=$2 + +revfile=/tmp/cvs2vendor_$$_rev +rm -f $revfile + +commentfile=/tmp/cvs2vendor_$$_comment +rm -f $commentfile + +srcdirs=`cd $tsrcdir && find . -type d -print | sed 's~^\.[/]*~~'` + +# the "" is a trick to get $tsrcdir itself without resorting to '.' +for ldir in "" $srcdirs; do + + srcdir=$tsrcdir/$ldir + dstdir=$tdstdir/$ldir + + # Loop over every RCS file in srcdir + # + for vfile in $srcdir/*,v; do + # get rid of the ",v" at the end of the name + file=`echo $vfile | sed -e 's/,v$//'` + bfile=`basename $file` + + if [ ! -d $dstdir ]; then + echo "making locally added directory $dstdir" + mkdir -p $dstdir + fi + if [ ! -f $dstdir/$bfile,v ]; then + echo "copying locally added file $dstdir/$bfile ..." + cp $vfile $dstdir + continue; + fi + + # work on each rev of that file in ascending order + rlog $file | grep "^revision [0-9][0-9]*\." | awk '{print $2}' | sed -e 's/\./ /g' | sort -n -u +0 +1 +2 +3 +4 +5 +6 +7 +8 | sed -e 's/ /./g' > $revfile + + for rev in `cat $revfile`; do + + case "$rev" in + 1.1) + newdate=`rlog -r$rev $file | grep "^date: " | awk '{printf("%s.%s\n",$2,$3); exit}' | sed -e 's~/~.~g' -e 's/:/./g' -e 's/;//' -e 's/^19//'` + olddate=`rlog -r1.1.1.1 $dstdir/$bfile | grep "^date: " | awk '{printf("%s.%s\n",$2,$3); exit}' | sed -e 's~/~.~g' -e 's/:/./g' -e 's/;//' -e 's/^19//'` + sed "s/$olddate/$newdate/" < $dstdir/$bfile,v > $dstdir/$bfile.x + mv -f $dstdir/$bfile.x $dstdir/$bfile,v + chmod -w $dstdir/$bfile,v + symname=`rlog -h $file | sed -e '1,/^symbolic names:/d' -e 's/[ ]*//g' | awk -F: '$2 == "'"$rev"'" {printf("-n%s:1.1.1.1\n",$1)}'` + if [ -n "$symname" ]; then + echo "tagging $file with $symname ..." + rcs $symname $dstdir/$bfile,v + if [ $? != 0 ]; then + echo ERROR - rcs $symname $dstdir/$bfile,v + exit 1 + fi + fi + continue # skip first rev.... + ;; + esac + + # get a lock on the destination local branch tip revision + co -r1 -l $dstdir/$bfile + if [ $? != 0 ]; then + echo ERROR - co -r1 -l $dstdir/$bfile + exit 1 + fi + rm -f $dstdir/$bfile + + # get file into current dir and get stats + date=`rlog -r$rev $file | grep "^date: " | awk '{printf("%s %s\n",$2,$3); exit}' | sed -e 's/;//'` + author=`rlog -r$rev $file | grep "^date: " | awk '{print $5; exit}' | sed -e 's/;//'` + + symname=`rlog -h $file | sed -e '1,/^symbolic names:/d' -e 's/[ ]*//g' | awk -F: '$2 == "'"$rev"'" {printf("-n%s\n",$1)}'` + + rlog -r$rev $file | sed -e '/^branches: /d' -e '1,/^date: /d' -e '/^===========/d' | awk '{if ((total += length($0) + 1) < 510) print $0}' > $commentfile + + echo "==> file $file, rev=$rev, date=$date, author=$author $symname" + + co -p -r$rev $file > $bfile + if [ $? != 0 ]; then + echo ERROR - co -p -r$rev $file + exit 1 + fi + + # check file into vendor repository... + ci -f -m"`cat $commentfile`" -d"$date" $symname -w"$author" $bfile $dstdir/$bfile,v + if [ $? != 0 ]; then + echo ERROR - ci -f -m"`cat $commentfile`" -d"$date" $symname -w"$author" $bfile $dstdir/$bfile,v + exit 1 + fi + rm -f $bfile + + # set the default branch to the trunk... + # XXX really only need to do this once.... + rcs -b1 $dstdir/$bfile + if [ $? != 0 ]; then + echo ERROR - rcs -b1 $dstdir/$bfile + exit 1 + fi + done + done +done + +echo cleaning up... +rm -f $commentfile +echo " Conversion Completed Successfully" + +exit 0 diff --git a/gnu/dist/cvs/contrib/cvs_acls.pl b/gnu/dist/cvs/contrib/cvs_acls.pl new file mode 100644 index 00000000000..c1d64e94877 --- /dev/null +++ b/gnu/dist/cvs/contrib/cvs_acls.pl @@ -0,0 +1,141 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# Access control lists for CVS. dgg@ksr.com (David G. Grubbs) +# +# CVS "commitinfo" for matching repository names, running the program it finds +# on the same line. More information is available in the CVS man pages. +# +# ==== INSTALLATION: +# +# To use this program as I intended, do the following four things: +# +# 0. Install PERL. :-) +# +# 1. Put one line, as the *only* non-comment line, in your commitinfo file: +# +# DEFAULT /usr/local/bin/cvs_acls +# +# 2. Install this file as /usr/local/bin/cvs_acls and make it executable. +# +# 3. Create a file named $CVSROOT/CVSROOT/avail. +# +# ==== FORMAT OF THE avail FILE: +# +# The avail file determines whether you may commit files. It contains lines +# read from top to bottom, keeping track of a single "bit". The "bit" +# defaults to "on". It can be turned "off" by "unavail" lines and "on" by +# "avail" lines. ==> Last one counts. +# +# Any line not beginning with "avail" or "unavail" is ignored. +# +# Lines beginning with "avail" or "unavail" are assumed to be '|'-separated +# triples: (All spaces and tabs are ignored in a line.) +# +# {avail.*,unavail.*} [| user,user,... [| repos,repos,...]] +# +# 1. String starting with "avail" or "unavail". +# 2. Optional, comma-separated list of usernames. +# 3. Optional, comma-separated list of repository pathnames. +# These are pathnames relative to $CVSROOT. They can be directories or +# filenames. A directory name allows access to all files and +# directories below it. +# +# Example: (Text from the ';;' rightward may not appear in the file.) +# +# unavail ;; Make whole repository unavailable. +# avail|dgg ;; Except for user "dgg". +# avail|fred, john|bin/ls ;; Except when "fred" or "john" commit to +# ;; the module whose repository is "bin/ls" +# +# PROGRAM LOGIC: +# +# CVS passes to @ARGV an absolute directory pathname (the repository +# appended to your $CVSROOT variable), followed by a list of filenames +# within that directory. +# +# We walk through the avail file looking for a line that matches both +# the username and repository. +# +# A username match is simply the user's name appearing in the second +# column of the avail line in a space-or-comma separate list. +# +# A repository match is either: +# - One element of the third column matches $ARGV[0], or some +# parent directory of $ARGV[0]. +# - Otherwise *all* file arguments ($ARGV[1..$#ARGV]) must be +# in the file list in one avail line. +# - In other words, using directory names in the third column of +# the avail file allows committing of any file (or group of +# files in a single commit) in the tree below that directory. +# - If individual file names are used in the third column of +# the avail file, then files must be committed individually or +# all files specified in a single commit must all appear in +# third column of a single avail line. +# + +$debug = 0; +$cvsroot = $ENV{'CVSROOT'}; +$availfile = $cvsroot . "/CVSROOT/avail"; +$myname = $ENV{"USER"} if !($myname = $ENV{"LOGNAME"}); + +eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';" + while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV)); +exit 255 if $die; # process any variable=value switches + +die "Must set CVSROOT\n" if !$cvsroot; +($repos = shift) =~ s:^$cvsroot/::; +grep($_ = $repos . '/' . $_, @ARGV); + +print "$$ Repos: $repos\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if $debug; + +$exit_val = 0; # Good Exit value + +$universal_off = 0; +open (AVAIL, $availfile) || exit(0); # It is ok for avail file not to exist +while (<AVAIL>) { + chop; + next if /^\s*\#/; + next if /^\s*$/; + ($flagstr, $u, $m) = split(/[\s,]*\|[\s,]*/, $_); + + # Skip anything not starting with "avail" or "unavail" and complain. + (print "Bad avail line: $_\n"), next + if ($flagstr !~ /^avail/ && $flagstr !~ /^unavail/); + + # Set which bit we are playing with. ('0' is OK == Available). + $flag = (($& eq "avail") ? 0 : 1); + + # If we find a "universal off" flag (i.e. a simple "unavail") remember it + $universal_off = 1 if ($flag && !$u && !$m); + + # $myname considered "in user list" if actually in list or is NULL + $in_user = (!$u || grep ($_ eq $myname, split(/[\s,]+/,$u))); + print "$$ \$myname($myname) in user list: $_\n" if $debug && $in_user; + + # Module matches if it is a NULL module list in the avail line. If module + # list is not null, we check every argument combination. + if (!($in_repo = !$m)) { + @tmp = split(/[\s,]+/,$m); + for $j (@tmp) { + # If the repos from avail is a parent(or equal) dir of $repos, OK + $in_repo = 1, last if ($repos eq $j || $repos =~ /^$j\//); + } + if (!$in_repo) { + $in_repo = 1; + for $j (@ARGV) { + last if !($in_repo = grep ($_ eq $j, @tmp)); + } + } + } + print "$$ \$repos($repos) in repository list: $_\n" if $debug && $in_repo; + + $exit_val = $flag if ($in_user && $in_repo); + print "$$ ==== \$exit_val = $exit_val\n$$ ==== \$flag = $flag\n" if $debug; +} +close(AVAIL); +print "$$ ==== \$exit_val = $exit_val\n" if $debug; +print "**** Access denied: Insufficient Karma ($myname|$repos)\n" if $exit_val; +print "**** Access allowed: Personal Karma exceeds Environmental Karma.\n" + if $universal_off && !$exit_val; +exit($exit_val); diff --git a/gnu/dist/cvs/contrib/cvscheck.man b/gnu/dist/cvs/contrib/cvscheck.man new file mode 100644 index 00000000000..2b90b49a14c --- /dev/null +++ b/gnu/dist/cvs/contrib/cvscheck.man @@ -0,0 +1,52 @@ +.\" Contributed by Lowell Skoog <fluke!lowell@uunet.uu.net> +.TH CVSCHECK LOCAL "4 March 1991" FLUKE +.SH NAME +cvscheck \- identify files added, changed, or removed in a CVS working +directory +.SH SYNOPSIS +.B cvscheck +.SH DESCRIPTION +This command is a housekeeping aid. It should be run in a working +directory that has been checked out using CVS. It identifies files +that have been added, changed, or removed in the working directory, but +not CVS +.BR commit ted. +It also determines whether the files have been CVS +.BR add ed +or CVS +.BR remove d. +For directories, this command determines only whether they have been +.BR add ed. +It operates in the current directory only. +.LP +This command provides information that is available using CVS +.B status +and CVS +.BR diff . +The advantage of +.B cvscheck +is that its output is very concise. It saves you the strain (and +potential error) of interpreting the output of CVS +.B status +and +.BR diff . +.LP +See +.BR cvs (local) +or +.BR cvshelp (local) +for instructions on how to add or remove a file or directory in a +CVS-controlled package. +.SH DIAGNOSTICS +The exit status is 0 if no files have been added, changed, or removed +from the current directory. Otherwise, the command returns a count of +the adds, changes, and deletes. +.SH SEE ALSO +.BR cvs (local), +.BR cvshelp (local) +.SH AUTHOR +Lowell Skoog +.br +Software Technology Group +.br +Technical Computing diff --git a/gnu/dist/cvs/contrib/cvscheck.sh b/gnu/dist/cvs/contrib/cvscheck.sh new file mode 100644 index 00000000000..f711b430e0f --- /dev/null +++ b/gnu/dist/cvs/contrib/cvscheck.sh @@ -0,0 +1,83 @@ +#! /bin/sh +# +# cvscheck - identify files added, changed, or removed +# in CVS working directory +# +# Contributed by Lowell Skoog <fluke!lowell@uunet.uu.net> +# +# This program should be run in a working directory that has been +# checked out using CVS. It identifies files that have been added, +# changed, or removed in the working directory, but not "cvs +# committed". It also determines whether the files have been "cvs +# added" or "cvs removed". For directories, it is only practical to +# determine whether they have been added. + +name=cvscheck +changes=0 + +# If we can't run CVS commands in this directory +cvs status . > /dev/null 2>&1 +if [ $? != 0 ] ; then + + # Bail out + echo "$name: there is no version here; bailing out" 1>&2 + exit 1 +fi + +# Identify files added to working directory +for file in .* * ; do + + # Skip '.' and '..' + if [ $file = '.' -o $file = '..' ] ; then + continue + fi + + # If a regular file + if [ -f $file ] ; then + if cvs status $file | grep -s '^From:[ ]*New file' ; then + echo "file added: $file - not CVS committed" + changes=`expr $changes + 1` + elif cvs status $file | grep -s '^From:[ ]*no entry for' ; then + echo "file added: $file - not CVS added, not CVS committed" + changes=`expr $changes + 1` + fi + + # Else if a directory + elif [ -d $file -a $file != CVS.adm ] ; then + + # Move into it + cd $file + + # If CVS commands don't work inside + cvs status . > /dev/null 2>&1 + if [ $? != 0 ] ; then + echo "directory added: $file - not CVS added" + changes=`expr $changes + 1` + fi + + # Move back up + cd .. + fi +done + +# Identify changed files +changedfiles=`cvs diff | egrep '^diff' | awk '{print $3}'` +for file in $changedfiles ; do + echo "file changed: $file - not CVS committed" + changes=`expr $changes + 1` +done + +# Identify files removed from working directory +removedfiles=`cvs status | egrep '^File:[ ]*no file' | awk '{print $4}'` + +# Determine whether each file has been cvs removed +for file in $removedfiles ; do + if cvs status $file | grep -s '^From:[ ]*-' ; then + echo "file removed: $file - not CVS committed" + else + echo "file removed: $file - not CVS removed, not CVS committed" + fi + changes=`expr $changes + 1` +done + +exit $changes diff --git a/gnu/dist/cvs/contrib/cvshelp.man b/gnu/dist/cvs/contrib/cvshelp.man new file mode 100644 index 00000000000..b166af69528 --- /dev/null +++ b/gnu/dist/cvs/contrib/cvshelp.man @@ -0,0 +1,561 @@ +.\" Contributed by Lowell Skoog <fluke!lowell@uunet.uu.net> +.\" Full space in nroff; half space in troff +.de SP +.if n .sp +.if t .sp .5 +.. +.\" Start a command example +.de XS +.SP +.in +.5i +.ft B +.nf +.. +.\" End a command example +.de XE +.fi +.ft P +.in -.5i +.SP +.. +.TH CVSHELP LOCAL "17 March 1991" FLUKE +.SH NAME +cvshelp \- advice on using the Concurrent Versions System +.SH DESCRIPTION +This man page is based on experience using CVS. +It is bound to change as we gain more experience. +If you come up with better advice than is found here, +contact the Software Technology +Group and we will add it to this page. +.SS "Getting Started" +Use the following steps to prepare to use CVS: +.TP +\(bu +Take a look at the CVS manual page to see what it can do for you, and +if it fits your environment (or can possibly be made to fit your +environment). +.XS +man cvs +.XE +If things look good, continue on... +.TP +\(bu +Setup the master source repository. Choose a directory with +ample disk space available for source files. This is where the RCS +`,v' files will be stored. Say you choose +.B /src/master +as the root +of your source repository. Make the +.SB CVSROOT.adm +directory in the root of the source repository: +.XS +mkdir /src/master/CVSROOT.adm +.XE +.TP +\(bu +Populate this directory with the +.I loginfo +and +.I modules +files from the +.B "/usr/doc/local/cvs" +directory. Edit these files to reflect your local source repository +environment \- they may be quite small initially, but will grow as +sources are added to your source repository. Turn these files into +RCS controlled files: +.XS +cd /src/master/CVSROOT.adm +ci \-m'Initial loginfo file' loginfo +ci \-m'Initial modules file' modules +.XE +.TP +\(bu +Run the command: +.XS +mkmodules /src/master/CVSROOT.adm +.XE +This will build the +.BR ndbm (3) +file for the modules database. +.TP +\(bu +Remember to edit the +.I modules +file manually when sources are checked +in with +.B checkin +or CVS +.BR add . +A copy of the +.I modules +file for editing can be retrieved with the command: +.XS +cvs checkout CVSROOT.adm +.XE +.TP +\(bu +Have all users of the CVS system set the +.SM CVSROOT +environment variable appropriately to reflect the placement of your +source repository. If the above example is used, the following +commands can be placed in a +.I .login +or +.I .profile +file: +.XS +setenv CVSROOT /src/master +.XE +for csh users, and +.XS +CVSROOT=/src/master; export CVSROOT +.XE +for sh users. +.SS "Placing Locally Written Sources Under CVS Control" +Say you want to place the `whizbang' sources under +CVS control. Say further that the sources have never +been under revision control before. +.TP +\(bu +Move the source hierarchy (lock, stock, and barrel) +into the master source repository: +.XS +mv ~/whizbang $CVSROOT +.XE +.TP +\(bu +Clean out unwanted object files: +.XS +cd $CVSROOT/whizbang +make clean +.XE +.TP +\(bu +Turn every file in the hierarchy into an RCS controlled file: +.XS +descend \-f 'ci \-t/dev/null \-m"Placed under CVS control" \-nV\fR\fIx\fR\fB_\fR\fIy\fR\fB *' +.XE +In this example, the initial release tag is \fBV\fIx\fB_\fIy\fR, +representing version \fIx\fR.\fIy\fR. +.LP +You can use CVS on sources that are already under RCS control. +The following example shows how. +In this example, the source package is called `skunkworks'. +.TP +\(bu +Move the source hierarchy into the master source +repository: +.XS +mv ~/skunkworks $CVSROOT +.XE +.TP +\(bu +Clean out unwanted object files: +.XS +cd $CVSROOT/skunkworks +make clean +.XE +.TP +\(bu +Clean out unwanted working files, leaving only the RCS `,v' files: +.XS +descend \-r rcsclean +.XE +Note: If any working files have been checked out and changed, +.B rcsclean +will fail. Check in the modified working files +and run the command again. +.TP +\(bu +Get rid of +.SB RCS +subdirectories. CVS does not use them. +.XS +descend \-r \-f 'mv RCS/*,v .' +descend \-r \-f 'rmdir RCS' +.XE +.TP +\(bu +Delete any unwanted files that remain in the source hierarchy. Then +make sure all files are under RCS control: +.XS +descend \-f 'ci \-t/dev/null \-m"Placed under CVS control" \-n\fR\fItag\fR\fB *' +.XE +.I tag +is the latest symbolic revision tag that you applied to your package +(if any). Note: This command will probably generate lots of error +messages (for directories and existing RCS files) that you can +ignore. +.SS "Placing a Third-Party Source Distribution Under CVS Control" +The +.B checkin +command checks third-party sources into CVS. The +difference between third-party sources and locally +written sources is that third-party sources must be checked into a +separate branch (called the +.IR "vendor branch" ) +of the RCS tree. This makes it possible to merge local changes to +the sources with later releases from the vendor. +.TP +\(bu +Save the original distribution kit somewhere. For example, if the +master source repository is +.B /src/master +the distribution kit could be saved in +.BR /src/dist . +Organize the distribution directory so that each release +is clearly identifiable. +.TP +\(bu +Unpack the package in a scratch directory, for example +.BR ~/scratch . +.TP +\(bu +Create a repository for the package. +In this example, the package is called `Bugs-R-Us 4.3'. +.XS +mkdir $CVSROOT/bugs +.XE +.TP +\(bu +Check in the unpacked files: +.XS +cd ~/scratch +checkin \-m 'Bugs-R-Us 4.3 distribution' bugs VENDOR V4_3 +.XE +There is nothing magic about the tag `VENDOR', which is applied to +the vendor branch. You can use whatever tag you want. `VENDOR' is a +useful convention. +.TP +\(bu +Never modify vendor files before checking them in. +Check in the files +.I exactly +as you unpacked them. +If you check in locally modified files, future vendor releases may +wipe out your local changes. +.SS "Working With CVS-Controlled Sources" +To use or edit the sources, you must check out a private copy. +For the following examples, the master files are assumed to reside in +.BR "$CVSROOT/behemoth" . +The working directory is +.BR "~/work" . +See +.BR cvs (local) +for more details on the commands mentioned below. +.TP +.I "To Check Out Working Files +Use CVS +.BR checkout : +.XS +cd ~/work +cvs checkout behemoth +.XE +There is nothing magic about the working directory. CVS will check +out sources anywhere you like. Once you have a working copy of the +sources, you can compile or edit them as desired. +.TP +.I "To Display Changes You Have Made" +Use CVS +.BR diff +to display detailed changes, equivalent to +.BR rcsdiff (local). +You can also use +.BR cvscheck (local) +to list files added, changed, and removed in +the directory, but not yet +.BR commit ted. +You must be in a directory containing working files. +.TP +.I "To Display Revision Information" +Use CVS +.BR log , +which is equivalent to +.BR rlog (local). +You must be in a directory containing working files. +.TP +.I "To Update Working Files" +Use CVS +.BR update +in a directory containing working files. +This command brings your working files up +to date with changes checked into the +master repository since you last checked out or updated +your files. +.TP +.I "To Check In Your Changes" +Use CVS +.BR commit +in a directory containing working files. +This command checks your changes into the master repository. +You can specify files by name or use +.XS +cvs commit \-a +.XE +to +.B commit +all the files you have changed. +.TP +.I "To Add a File" +Add the file to the working directory. +Use CVS +.B add +to mark the file as added. +Use CVS +.B commit +to add the file to the master repository. +.TP +.I "To Remove a File" +Remove the file from the working directory. +Use CVS +.B remove +to mark the file as removed. +Use CVS +.B commit +to move the file from its current location in the master repository +to the CVS +.IR Attic +directory. +.TP +.I "To Add a Directory" +Add the directory to the working directory. +Use CVS +.B add +to add the directory to the master repository. +.TP +.I "To Remove a Directory" +.br +You shouldn't remove directories under CVS. You should instead remove +their contents and then prune them (using the +.B \-f +and +.B \-p +options) when you +.B checkout +or +.B update +your working files. +.TP +.I "To Tag a Release" +Use CVS +.B tag +to apply a symbolic tag to the latest revision of each file in the +master repository. For example: +.XS +cvs tag V2_1 behemoth +.XE +.TP +.I "To Retrieve an Exact Copy of a Previous Release" +During a CVS +.B checkout +or +.BR update , +use the +.B \-r +option to retrieve revisions associated with a symbolic tag. +Use the +.B \-f +option to ignore all RCS files that do not contain the +tag. +Use the +.B \-p +option to prune directories that wind up empty because none +of their files matched the tag. Example: +.XS +cd ~/work +cvs checkout \-r V2_1 \-f \-p behemoth +.XE +.SS "Logging Changes" +It is a good idea to keep a change log together with the +sources. As a minimum, the change log should name and describe each +tagged release. The change log should also be under CVS control and +should be tagged along with the sources. +.LP +.BR cvslog (local) +can help. This command logs +changes reported during CVS +.B commit +operations. It automatically +updates a change log file in your working directory. When you are +finished making changes, you (optionally) edit the change log file and +then commit it to the master repository. +.LP +Note: You must edit the change log to describe a new release +and +.B commit +it to the master repository +.I before +.BR tag ging +the release using CVS. Otherwise, the release description will not be +included in the tagged package. +.LP +See +.BR cvslog (local) +for more information. +.SS "Merging a Subsequent Third-Party Distribution" +The initial steps in this process are identical to placing a +third-party distribution under CVS for the first time: save the +distribution kit and unpack the package in a scratch directory. From +that point the steps diverge. +The following example considers release 5.0 of the +Bugs-R-Us package. +.TP +\(bu +Check in the sources after unpacking them: +.XS +cd ~/scratch +checkin \-m 'Bugs-R-Us 5.0 distribution' bugs VENDOR V5_0 \\ + | tee ~/WARNINGS +.XE +It is important to save the output of +.B checkin +in a file +because it lists the sources that have been locally modified. +It is best to save the file in a different directory (for example, +your home directory). Otherwise, +.B checkin +will try to check it into the master repository. +.TP +\(bu +In your usual working directory, check out a fresh copy of the +distribution that you just checked in. +.XS +cd ~/work +cvs checkout \-r VENDOR bugs +.XE +The +.B checkout +command shown above retrieves the latest revision on the vendor branch. +.TP +\(bu +See the `WARNINGS' file for a list of all locally modified +sources. +For each locally modified source, +look at the differences between +the new distribution and the latest local revision: +.XS +cvs diff \-r \fR\fILocalRev file\fR\fB +.XE +In this command, +.I LocalRev +is the latest +numeric or symbolic revision +on the RCS trunk of +.IR file . +You can use CVS +.B log +to get the revision history. +.TP +\(bu +If your local modifications to a file have been incorporated into +the vendor's distribution, then you should reset the default RCS +branch for that file to the vendor branch. CVS doesn't provide a +mechanism to do this. You have to do it by hand in the master +repository: +.XS +rcs \-bVENDOR \fR\fIfile\fR\fB,v +.XE +.TP +\(bu +If your local modifications need to be merged with the +new distribution, use CVS +.B join +to do it: +.XS +cvs join \-r VENDOR \fR\fIfile\fR\fB +.XE +The resulting file will be placed in your working directory. +Edit it to resolve any overlaps. +.TP +\(bu +Test the merged package. +.TP +\(bu +Commit all modified files to the repository: +.XS +cvs commit \-a +.XE +.TP +\(bu +Tag the repository with a new local tag. +.SS "Applying Patches to Third-Party Sources" +Patches are handled in a manner very similar to complete +third-party distributions. This example considers patches applied to +Bugs-R-Us release 5.0. +.TP +\(bu +Save the patch files together with the distribution kit +to which they apply. +The patch file names should clearly indicate the patch +level. +.TP +\(bu +In a scratch directory, check out the last `clean' vendor copy \- the +highest revision on the vendor branch with +.IR "no local changes" : +.XS +cd ~/scratch +cvs checkout \-r VENDOR bugs +.XE +.TP +\(bu +Use +.BR patch (local) +to apply the patches. You should now have an image of the +vendor's software just as though you had received a complete, +new release. +.TP +\(bu +Proceed with the steps described for merging a subsequent third-party +distribution. +.TP +\(bu +Note: When you get to the step that requires you +to check out the new distribution after you have +checked it into the vendor branch, you should move to a different +directory. Do not attempt to +.B checkout +files in the directory in +which you applied the patches. If you do, CVS will try to merge the +changes that you made during patching with the version being checked +out and things will get very confusing. Instead, +go to a different directory (like your working directory) and +check out the files there. +.SS "Advice to Third-Party Source Hackers" +As you can see from the preceding sections, merging local changes +into third-party distributions remains difficult, and probably +always will. This fact suggests some guidelines: +.TP +\(bu +Minimize local changes. +.I Never +make stylistic changes. +Change makefiles only as much as needed for installation. Avoid +overhauling anything. Pray that the vendor does the same. +.TP +\(bu +Avoid renaming files or moving them around. +.TP +\(bu +Put independent, locally written files like help documents, local +tools, or man pages in a sub-directory called `local-additions'. +Locally written files that are linked into an existing executable +should be added right in with the vendor's sources (not in a +`local-additions' directory). +If, in the future, +the vendor distributes something +equivalent to your locally written files +you can CVS +.B remove +the files from the `local-additions' directory at that time. +.SH SEE ALSO +.BR cvs (local), +.BR checkin (local), +.BR cvslog (local), +.BR cvscheck (local) +.SH AUTHOR +Lowell Skoog +.br +Software Technology Group +.br +Technical Computing diff --git a/gnu/dist/cvs/contrib/descend.man b/gnu/dist/cvs/contrib/descend.man new file mode 100644 index 00000000000..0434ca8b43c --- /dev/null +++ b/gnu/dist/cvs/contrib/descend.man @@ -0,0 +1,114 @@ +.TH DESCEND 1 "31 March 1992" +.SH NAME +descend \- walk directory tree and execute a command at each node +.SH SYNOPSIS +.B descend +[ +.B \-afqrv +] +.I command +[ +.I directory +\&.\|.\|. +] +.SH DESCRIPTION +.B descend +walks down a directory tree and executes a command at each node. It +is not as versatile as +.BR find (1), +but it has a simpler syntax. If no +.I directory +is specified, +.B descend +starts at the current one. +.LP +Unlike +.BR find , +.B descend +can be told to skip the special directories associated with RCS, +CVS, and SCCS. This makes +.B descend +especially handy for use with these packages. It can be used with +other commands too, of course. +.LP +.B descend +is a poor man's way to make any command recursive. Note: +.B descend +does not follow symbolic links to directories unless they are +specified on the command line. +.SH OPTIONS +.TP 15 +.B \-a +.I All. +Descend into directories that begin with '.'. +.TP +.B \-f +.I Force. +Ignore errors during descent. Normally, +.B descend +quits when an error occurs. +.TP +.B \-q +.I Quiet. +Suppress the message `In directory +.IR directory ' +that is normally printed during the descent. +.TP +.B \-r +.I Restricted. +Don't descend into the special directories +.SB RCS, +.SB CVS, +.SB CVS.adm, +and +.SB SCCS. +.TP +.B \-v +.I Verbose. +Print +.I command +before executing it. +.SH EXAMPLES +.TP 15 +.B "descend ls" +Cheap substitute for `ls -R'. +.TP 15 +.B "descend -f 'rm *' tree" +Strip `tree' of its leaves. This command descends the `tree' +directory, removing all regular files. Since +.BR rm (1) +does not remove directories, this command leaves the directory +structure of `tree' intact, but denuded. The +.B \-f +option is required to keep +.B descend +from quitting. You could use `rm \-f' instead. +.TP +.B "descend -r 'co RCS/*'" /project/src/ +Check out every RCS file under the directory +.BR "/project/src" . +.TP +.B "descend -r 'cvs diff'" +Perform CVS `diff' operation on every directory below (and including) +the current one. +.SH DIAGNOSTICS +Returns 1 if errors occur (and the +.B \-f +option is not used). Otherwise returns 0. +.SH SEE ALSO +.BR find (1), +.BR rcsintro (1), +.BR cvs (1), +.BR sccs (1) +.SH AUTHOR +Lowell Skoog +.br +Software Technology Group +.br +John Fluke Mfg. Co., Inc. +.SH BUGS +Shell metacharacters in +.I command +may have bizarre effects. In particular, compound commands +(containing ';', '[', and ']' characters) will not work. It is best +to enclose complicated commands in single quotes \(aa\ \(aa. diff --git a/gnu/dist/cvs/contrib/descend.sh b/gnu/dist/cvs/contrib/descend.sh new file mode 100644 index 00000000000..039a7a3d3ee --- /dev/null +++ b/gnu/dist/cvs/contrib/descend.sh @@ -0,0 +1,115 @@ +#! /bin/sh +# +# descend - walk down a directory tree and execute a command at each node + +fullname=$0 +name=descend +usage="Usage: $name [-afqrv] command [directory ...]\n +\040\040-a\040\040All: descend into directories starting with '.'\n +\040\040-f\040\040Force: ignore errors during descent\n +\040\040-q\040\040Quiet: don't print directory names\n +\040\040-r\040\040Restricted: don't descend into RCS, CVS.adm, SCCS directories\n +\040\040-v\040\040Verbose: print command before executing it" + +# Scan for options +while getopts afqrv option; do + case $option in + a) + alldirs=$option + options=$options" "-$option + ;; + f) + force=$option + options=$options" "-$option + ;; + q) + verbose= + quiet=$option + options=$options" "-$option + ;; + r) + restricted=$option + options=$options" "-$option + ;; + v) + verbose=$option + quiet= + options=$options" "-$option + ;; + \?) + /usr/5bin/echo $usage 1>&2 + exit 1 + ;; + esac +done +shift `expr $OPTIND - 1` + +# Get command to execute +if [ $# -lt 1 ] ; then + /usr/5bin/echo $usage 1>&2 + exit 1 +else + command=$1 + shift +fi + +# If no directory specified, use '.' +if [ $# -lt 1 ] ; then + default_dir=. +fi + +# For each directory specified +for dir in $default_dir "$@" ; do + + # Spawn sub-shell so we return to starting directory afterward + (cd $dir + + # Execute specified command + if [ -z "$quiet" ] ; then + echo In directory `hostname`:`pwd` + fi + if [ -n "$verbose" ] ; then + echo $command + fi + eval "$command" || if [ -z "$force" ] ; then exit 1; fi + + # Collect dot file names if necessary + if [ -n "$alldirs" ] ; then + dotfiles=.* + else + dotfiles= + fi + + # For each file in current directory + for file in $dotfiles * ; do + + # Skip '.' and '..' + if [ "$file" = "." -o "$file" = ".." ] ; then + continue + fi + + # If a directory but not a symbolic link + if [ -d "$file" -a ! -h "$file" ] ; then + + # If not skipping this type of directory + if [ \( "$file" != "RCS" -a \ + "$file" != "SCCS" -a \ + "$file" != "CVS" -a \ + "$file" != "CVS.adm" \) \ + -o -z "$restricted" ] ; then + + # Recursively descend into it + $fullname $options "$command" "$file" \ + || if [ -z "$force" ] ; then exit 1; fi + fi + + # Else if a directory AND a symbolic link + elif [ -d "$file" -a -h "$file" ] ; then + + if [ -z "$quiet" ] ; then + echo In directory `hostname`:`pwd`/$file: symbolic link: skipping + fi + fi + done + ) || if [ -z "$force" ] ; then exit 1; fi +done diff --git a/gnu/dist/cvs/contrib/dirfns.shar b/gnu/dist/cvs/contrib/dirfns.shar new file mode 100644 index 00000000000..8324c4198e3 --- /dev/null +++ b/gnu/dist/cvs/contrib/dirfns.shar @@ -0,0 +1,481 @@ +echo 'directory.3': +sed 's/^X//' >'directory.3' <<'!' +X.TH DIRECTORY 3 imported +X.DA 9 Oct 1985 +X.SH NAME +Xopendir, readdir, telldir, seekdir, rewinddir, closedir \- high-level directory operations +X.SH SYNOPSIS +X.B #include <sys/types.h> +X.br +X.B #include <ndir.h> +X.PP +X.SM +X.B DIR +X.B *opendir(filename) +X.br +X.B char *filename; +X.PP +X.SM +X.B struct direct +X.B *readdir(dirp) +X.br +X.B DIR *dirp; +X.PP +X.SM +X.B long +X.B telldir(dirp) +X.br +X.B DIR *dirp; +X.PP +X.SM +X.B seekdir(dirp, loc) +X.br +X.B DIR *dirp; +X.br +X.B long loc; +X.PP +X.SM +X.B rewinddir(dirp) +X.br +X.B DIR *dirp; +X.PP +X.SM +X.B closedir(dirp) +X.br +X.B DIR *dirp; +X.SH DESCRIPTION +XThis library provides high-level primitives for directory scanning, +Xsimilar to those available for 4.2BSD's (very different) directory system. +X.\"The purpose of this library is to simulate +X.\"the new flexible length directory names of 4.2bsd UNIX +X.\"on top of the old directory structure of v7. +XIt incidentally provides easy portability to and from 4.2BSD (insofar +Xas such portability is not compromised by other 4.2/VAX dependencies). +X.\"It allows programs to be converted immediately +X.\"to the new directory access interface, +X.\"so that they need only be relinked +X.\"when moved to 4.2bsd. +X.\"It is obtained with the loader option +X.\".BR \-lndir . +X.PP +X.I Opendir +Xopens the directory named by +X.I filename +Xand associates a +X.I directory stream +Xwith it. +X.I Opendir +Xreturns a pointer to be used to identify the +X.I directory stream +Xin subsequent operations. +XThe pointer +X.SM +X.B NULL +Xis returned if +X.I filename +Xcannot be accessed or is not a directory. +X.PP +X.I Readdir +Xreturns a pointer to the next directory entry. +XIt returns +X.B NULL +Xupon reaching the end of the directory or detecting +Xan invalid +X.I seekdir +Xoperation. +X.PP +X.I Telldir +Xreturns the current location associated with the named +X.I directory stream. +X.PP +X.I Seekdir +Xsets the position of the next +X.I readdir +Xoperation on the +X.I directory stream. +XThe new position reverts to the one associated with the +X.I directory stream +Xwhen the +X.I telldir +Xoperation was performed. +XValues returned by +X.I telldir +Xare good only for the lifetime of the DIR pointer from +Xwhich they are derived. +XIf the directory is closed and then reopened, +Xthe +X.I telldir +Xvalue may be invalidated +Xdue to undetected directory compaction in 4.2BSD. +XIt is safe to use a previous +X.I telldir +Xvalue immediately after a call to +X.I opendir +Xand before any calls to +X.I readdir. +X.PP +X.I Rewinddir +Xresets the position of the named +X.I directory stream +Xto the beginning of the directory. +X.PP +X.I Closedir +Xcauses the named +X.I directory stream +Xto be closed, +Xand the structure associated with the DIR pointer to be freed. +X.PP +XA +X.I direct +Xstructure is as follows: +X.PP +X.RS +X.nf +Xstruct direct { +X /* unsigned */ long d_ino; /* inode number of entry */ +X unsigned short d_reclen; /* length of this record */ +X unsigned short d_namlen; /* length of string in d_name */ +X char d_name[MAXNAMLEN + 1]; /* name must be no longer than this */ +X}; +X.fi +X.RE +X.PP +XThe +X.I d_reclen +Xfield is meaningless in non-4.2BSD systems and should be ignored. +XThe use of a +X.I long +Xfor +X.I d_ino +Xis also a 4.2BSDism; +X.I ino_t +X(see +X.IR types (5)) +Xshould be used elsewhere. +XThe macro +X.I DIRSIZ(dp) +Xgives the minimum memory size needed to hold the +X.I direct +Xvalue pointed to by +X.IR dp , +Xwith the minimum necessary allocation for +X.IR d_name . +X.PP +XThe preferred way to search the current directory for entry ``name'' is: +X.PP +X.RS +X.nf +X len = strlen(name); +X dirp = opendir("."); +X if (dirp == NULL) { +X fprintf(stderr, "%s: can't read directory .\\n", argv[0]); +X return NOT_FOUND; +X } +X while ((dp = readdir(dirp)) != NULL) +X if (dp->d_namlen == len && strcmp(dp->d_name, name) == 0) { +X closedir(dirp); +X return FOUND; +X } +X closedir(dirp); +X return NOT_FOUND; +X.RE +X.\".SH LINKING +X.\"This library is accessed by specifying ``-lndir'' as the +X.\"last argument to the compile line, e.g.: +X.\".PP +X.\" cc -I/usr/include/ndir -o prog prog.c -lndir +X.SH "SEE ALSO" +Xopen(2), +Xclose(2), +Xread(2), +Xlseek(2) +X.SH HISTORY +XWritten by +XKirk McKusick at Berkeley (ucbvax!mckusick). +XMiscellaneous bug fixes from elsewhere. +XThe size of the data structure has been decreased to avoid excessive +Xspace waste under V7 (where filenames are 14 characters at most). +XFor obscure historical reasons, the include file is also available +Xas +X.IR <ndir/sys/dir.h> . +XThe Berkeley version lived in a separate library (\fI\-lndir\fR), +Xwhereas ours is +Xpart of the C library, although the separate library is retained to +Xmaximize compatibility. +X.PP +XThis manual page has been substantially rewritten to be informative in +Xthe absence of a 4.2BSD manual. +X.SH BUGS +XThe +X.I DIRSIZ +Xmacro actually wastes a bit of space due to some padding requirements +Xthat are an artifact of 4.2BSD. +X.PP +XThe returned value of +X.I readdir +Xpoints to a static area that will be overwritten by subsequent calls. +X.PP +XThere are some unfortunate name conflicts with the \fIreal\fR V7 +Xdirectory structure definitions. +! +echo 'dir.h': +sed 's/^X//' >'dir.h' <<'!' +X/* dir.h 4.4 82/07/25 */ +X +X/* +X * A directory consists of some number of blocks of DIRBLKSIZ +X * bytes, where DIRBLKSIZ is chosen such that it can be transferred +X * to disk in a single atomic operation (e.g. 512 bytes on most machines). +X * +X * Each DIRBLKSIZ byte block contains some number of directory entry +X * structures, which are of variable length. Each directory entry has +X * a struct direct at the front of it, containing its inode number, +X * the length of the entry, and the length of the name contained in +X * the entry. These are followed by the name padded to a 4 byte boundary +X * with null bytes. All names are guaranteed null terminated. +X * The maximum length of a name in a directory is MAXNAMLEN. +X * +X * The macro DIRSIZ(dp) gives the amount of space required to represent +X * a directory entry. Free space in a directory is represented by +X * entries which have dp->d_reclen >= DIRSIZ(dp). All DIRBLKSIZ bytes +X * in a directory block are claimed by the directory entries. This +X * usually results in the last entry in a directory having a large +X * dp->d_reclen. When entries are deleted from a directory, the +X * space is returned to the previous entry in the same directory +X * block by increasing its dp->d_reclen. If the first entry of +X * a directory block is free, then its dp->d_ino is set to 0. +X * Entries other than the first in a directory do not normally have +X * dp->d_ino set to 0. +X */ +X#define DIRBLKSIZ 512 +X#ifdef VMUNIX +X#define MAXNAMLEN 255 +X#else +X#define MAXNAMLEN 14 +X#endif +X +Xstruct direct { +X /* unsigned */ long d_ino; /* inode number of entry */ +X unsigned short d_reclen; /* length of this record */ +X unsigned short d_namlen; /* length of string in d_name */ +X char d_name[MAXNAMLEN + 1]; /* name must be no longer than this */ +X}; +X +X/* +X * The DIRSIZ macro gives the minimum record length which will hold +X * the directory entry. This requires the amount of space in struct direct +X * without the d_name field, plus enough space for the name with a terminating +X * null byte (dp->d_namlen+1), rounded up to a 4 byte boundary. +X */ +X#undef DIRSIZ +X#define DIRSIZ(dp) \ +X ((sizeof (struct direct) - (MAXNAMLEN+1)) + (((dp)->d_namlen+1 + 3) &~ 3)) +X +X#ifndef KERNEL +X/* +X * Definitions for library routines operating on directories. +X */ +Xtypedef struct _dirdesc { +X int dd_fd; +X long dd_loc; +X long dd_size; +X char dd_buf[DIRBLKSIZ]; +X} DIR; +X#ifndef NULL +X#define NULL 0 +X#endif +Xextern DIR *opendir(); +Xextern struct direct *readdir(); +Xextern long telldir(); +X#ifdef void +Xextern void seekdir(); +Xextern void closedir(); +X#endif +X#define rewinddir(dirp) seekdir((dirp), (long)0) +X#endif KERNEL +! +echo 'makefile': +sed 's/^X//' >'makefile' <<'!' +XDIR = closedir.o opendir.o readdir.o seekdir.o telldir.o +XCFLAGS=-O -I. -Dvoid=int +XDEST=.. +X +Xall: $(DIR) +X +Xmv: $(DIR) +X mv $(DIR) $(DEST) +X +Xcpif: dir.h +X cp dir.h /usr/include/ndir.h +X +Xclean: +X rm -f *.o +! +echo 'closedir.c': +sed 's/^X//' >'closedir.c' <<'!' +Xstatic char sccsid[] = "@(#)closedir.c 4.2 3/10/82"; +X +X#include <sys/types.h> +X#include <dir.h> +X +X/* +X * close a directory. +X */ +Xvoid +Xclosedir(dirp) +X register DIR *dirp; +X{ +X close(dirp->dd_fd); +X dirp->dd_fd = -1; +X dirp->dd_loc = 0; +X free((char *)dirp); +X} +! +echo 'opendir.c': +sed 's/^X//' >'opendir.c' <<'!' +X/* Copyright (c) 1982 Regents of the University of California */ +X +Xstatic char sccsid[] = "@(#)opendir.c 4.4 11/12/82"; +X +X#include <sys/types.h> +X#include <sys/stat.h> +X#include <dir.h> +X +X/* +X * open a directory. +X */ +XDIR * +Xopendir(name) +X char *name; +X{ +X register DIR *dirp; +X register int fd; +X struct stat statbuf; +X char *malloc(); +X +X if ((fd = open(name, 0)) == -1) +X return NULL; +X if (fstat(fd, &statbuf) == -1 || !(statbuf.st_mode & S_IFDIR)) { +X close(fd); +X return NULL; +X } +X if ((dirp = (DIR *)malloc(sizeof(DIR))) == NULL) { +X close (fd); +X return NULL; +X } +X dirp->dd_fd = fd; +X dirp->dd_loc = 0; +X dirp->dd_size = 0; /* so that telldir will work before readdir */ +X return dirp; +X} +! +echo 'readdir.c': +sed 's/^X//' >'readdir.c' <<'!' +X/* Copyright (c) 1982 Regents of the University of California */ +X +Xstatic char sccsid[] = "@(#)readdir.c 4.3 8/8/82"; +X +X#include <sys/types.h> +X#include <dir.h> +X +X/* +X * read an old stlye directory entry and present it as a new one +X */ +X#define ODIRSIZ 14 +X +Xstruct olddirect { +X ino_t od_ino; +X char od_name[ODIRSIZ]; +X}; +X +X/* +X * get next entry in a directory. +X */ +Xstruct direct * +Xreaddir(dirp) +X register DIR *dirp; +X{ +X register struct olddirect *dp; +X static struct direct dir; +X +X for (;;) { +X if (dirp->dd_loc == 0) { +X dirp->dd_size = read(dirp->dd_fd, dirp->dd_buf, +X DIRBLKSIZ); +X if (dirp->dd_size <= 0) { +X dirp->dd_size = 0; +X return NULL; +X } +X } +X if (dirp->dd_loc >= dirp->dd_size) { +X dirp->dd_loc = 0; +X continue; +X } +X dp = (struct olddirect *)(dirp->dd_buf + dirp->dd_loc); +X dirp->dd_loc += sizeof(struct olddirect); +X if (dp->od_ino == 0) +X continue; +X dir.d_ino = dp->od_ino; +X strncpy(dir.d_name, dp->od_name, ODIRSIZ); +X dir.d_name[ODIRSIZ] = '\0'; /* insure null termination */ +X dir.d_namlen = strlen(dir.d_name); +X dir.d_reclen = DIRBLKSIZ; +X return (&dir); +X } +X} +! +echo 'seekdir.c': +sed 's/^X//' >'seekdir.c' <<'!' +Xstatic char sccsid[] = "@(#)seekdir.c 4.9 3/25/83"; +X +X#include <sys/param.h> +X#include <dir.h> +X +X/* +X * seek to an entry in a directory. +X * Only values returned by "telldir" should be passed to seekdir. +X */ +Xvoid +Xseekdir(dirp, loc) +X register DIR *dirp; +X long loc; +X{ +X long curloc, base, offset; +X struct direct *dp; +X extern long lseek(); +X +X curloc = telldir(dirp); +X if (loc == curloc) +X return; +X base = loc & ~(DIRBLKSIZ - 1); +X offset = loc & (DIRBLKSIZ - 1); +X (void) lseek(dirp->dd_fd, base, 0); +X dirp->dd_size = 0; +X dirp->dd_loc = 0; +X while (dirp->dd_loc < offset) { +X dp = readdir(dirp); +X if (dp == NULL) +X return; +X } +X} +! +echo 'telldir.c': +sed 's/^X//' >'telldir.c' <<'!' +Xstatic char sccsid[] = "@(#)telldir.c 4.1 2/21/82"; +X +X#include <sys/types.h> +X#include <dir.h> +X +X/* +X * return a pointer into a directory +X */ +Xlong +Xtelldir(dirp) +X DIR *dirp; +X{ +X long lseek(); +X +X return (lseek(dirp->dd_fd, 0L, 1) - dirp->dd_size + dirp->dd_loc); +X} +! +echo done diff --git a/gnu/dist/cvs/contrib/intro.doc b/gnu/dist/cvs/contrib/intro.doc new file mode 100644 index 00000000000..a6d4ec12338 --- /dev/null +++ b/gnu/dist/cvs/contrib/intro.doc @@ -0,0 +1,112 @@ +Date: Tue, 16 Jun 1992 17:05:23 +0200 +From: Steven.Pemberton@cwi.nl +Message-Id: <9206161505.AA06927.steven@sijs.cwi.nl> +To: berliner@Sun.COM +Subject: cvs + +INTRODUCTION TO USING CVS + + CVS is a system that lets groups of people work simultaneously on + groups of files (for instance program sources). + + It works by holding a central 'repository' of the most recent version + of the files. You may at any time create a personal copy of these + files; if at a later date newer versions of the files are put in the + repository, you can 'update' your copy. + + You may edit your copy of the files freely. If new versions of the + files have been put in the repository in the meantime, doing an update + merges the changes in the central copy into your copy. + (It can be that when you do an update, the changes in the + central copy clash with changes you have made in your own + copy. In this case cvs warns you, and you have to resolve the + clash in your copy.) + + When you are satisfied with the changes you have made in your copy of + the files, you can 'commit' them into the central repository. + (When you do a commit, if you haven't updated to the most + recent version of the files, cvs tells you this; then you have + to first update, resolve any possible clashes, and then redo + the commit.) + +USING CVS + + Suppose that a number of repositories have been stored in + /usr/src/cvs. Whenever you use cvs, the environment variable + CVSROOT must be set to this (for some reason): + + CVSROOT=/usr/src/cvs + export CVSROOT + +TO CREATE A PERSONAL COPY OF A REPOSITORY + + Suppose you want a copy of the files in repository 'views' to be + created in your directory src. Go to the place where you want your + copy of the directory, and do a 'checkout' of the directory you + want: + + cd $HOME/src + cvs checkout views + + This creates a directory called (in this case) 'views' in the src + directory, containing a copy of the files, which you may now work + on to your heart's content. + +TO UPDATE YOUR COPY + + Use the command 'cvs update'. + + This will update your copy with any changes from the central + repository, telling you which files have been updated (their names + are displayed with a U before them), and which have been modified + by you and not yet committed (preceded by an M). You will be + warned of any files that contain clashes, the clashes will be + marked in the file surrounded by lines of the form <<<< and >>>>. + +TO COMMIT YOUR CHANGES + + Use the command 'cvs commit'. + + You will be put in an editor to make a message that describes the + changes that you have made (for future reference). Your changes + will then be added to the central copy. + +ADDING AND REMOVING FILES + + It can be that the changes you want to make involve a completely + new file, or removing an existing one. The commands to use here + are: + + cvs add <filename> + cvs remove <filename> + + You still have to do a commit after these commands. You may make + any number of new files in your copy of the repository, but they + will not be committed to the central copy unless you do a 'cvs add'. + +OTHER USEFUL COMMANDS AND HINTS + + To see the commit messages for files, and who made them, use: + + cvs log [filenames] + + To see the differences between your version and the central version: + + cvs diff [filenames] + + To give a file a new name, rename it and do an add and a remove. + + To lose your changes and go back to the version from the + repository, delete the file and do an update. + + After an update where there have been clashes, your original + version of the file is saved as .#file.version. + + All the cvs commands mentioned accept a flag '-n', that doesn't do + the action, but lets you see what would happen. For instance, you + can use 'cvs -n update' to see which files would be updated. + +MORE INFORMATION + + This is necessarily a very brief introduction. See the manual page + (man cvs) for full details. diff --git a/gnu/dist/cvs/contrib/log.pl b/gnu/dist/cvs/contrib/log.pl new file mode 100644 index 00000000000..3a8ad992b60 --- /dev/null +++ b/gnu/dist/cvs/contrib/log.pl @@ -0,0 +1,177 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# XXX: FIXME: handle multiple '-f logfile' arguments +# +# XXX -- I HATE Perl! This *will* be re-written in shell/awk/sed soon! +# + +# Usage: log.pl [[-m user] ...] [-s] -f logfile 'dirname file ...' +# +# -m user - for each user to receive cvs log reports +# (multiple -m's permitted) +# -s - to prevent "cvs status -v" messages +# -f logfile - for the logfile to append to (mandatory, +# but only one logfile can be specified). + +# here is what the output looks like: +# +# From: woods@kuma.domain.top +# Subject: CVS update: testmodule +# +# Date: Wednesday November 23, 1994 @ 14:15 +# Author: woods +# +# Update of /local/src-CVS/testmodule +# In directory kuma:/home/kuma/woods/work.d/testmodule +# +# Modified Files: +# test3 +# Added Files: +# test6 +# Removed Files: +# test4 +# Log Message: +# - wow, what a test +# +# (and for each file the "cvs status -v" output is appended unless -s is used) +# +# ================================================================== +# File: test3 Status: Up-to-date +# +# Working revision: 1.41 Wed Nov 23 14:15:59 1994 +# Repository revision: 1.41 /local/src-CVS/cvs/testmodule/test3,v +# Sticky Options: -ko +# +# Existing Tags: +# local-v2 (revision: 1.7) +# local-v1 (revision: 1.1.1.2) +# CVS-1_4A2 (revision: 1.1.1.2) +# local-v0 (revision: 1.2) +# CVS-1_4A1 (revision: 1.1.1.1) +# CVS (branch: 1.1.1) + +$cvsroot = $ENV{'CVSROOT'}; + +# turn off setgid +# +$) = $(; + +$dostatus = 1; + +# parse command line arguments +# +while (@ARGV) { + $arg = shift @ARGV; + + if ($arg eq '-m') { + $users = "$users " . shift @ARGV; + } elsif ($arg eq '-f') { + ($logfile) && die "Too many '-f' args"; + $logfile = shift @ARGV; + } elsif ($arg eq '-s') { + $dostatus = 0; + } else { + ($donefiles) && die "Too many arguments!\n"; + $donefiles = 1; + @files = split(/ /, $arg); + } +} + +# the first argument is the module location relative to $CVSROOT +# +$modulepath = shift @files; + +$mailcmd = "| Mail -s 'CVS update: $modulepath'"; + +# Initialise some date and time arrays +# +@mos = (January,February,March,April,May,June,July,August,September, + October,November,December); +@days = (Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday); + +($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime; +$year += 1900; + +# get a login name for the guy doing the commit.... +# +$login = getlogin || (getpwuid($<))[0] || "nobody"; + +# open log file for appending +# +open(OUT, ">>" . $logfile) || die "Could not open(" . $logfile . "): $!\n"; + +# send mail, if there's anyone to send to! +# +if ($users) { + $mailcmd = "$mailcmd $users"; + open(MAIL, $mailcmd) || die "Could not Exec($mailcmd): $!\n"; +} + +# print out the log Header +# +print OUT "\n"; +print OUT "****************************************\n"; +print OUT "Date:\t$days[$wday] $mos[$mon] $mday, $year @ $hour:" . sprintf("%02d", $min) . "\n"; +print OUT "Author:\t$login\n\n"; + +if (MAIL) { + print MAIL "\n"; + print MAIL "Date:\t$days[$wday] $mos[$mon] $mday, $year @ $hour:" . sprintf("%02d", $min) . "\n"; + print MAIL "Author:\t$login\n\n"; +} + +# print the stuff from logmsg that comes in on stdin to the logfile +# +open(IN, "-"); +while (<IN>) { + print OUT $_; + if (MAIL) { + print MAIL $_; + } +} +close(IN); + +print OUT "\n"; + +# after log information, do an 'cvs -Qq status -v' on each file in the arguments. +# +if ($dostatus != 0) { + while (@files) { + $file = shift @files; + if ($file eq "-") { + print OUT "[input file was '-']\n"; + if (MAIL) { + print MAIL "[input file was '-']\n"; + } + last; + } + $pid = open(RCS, "-|"); + if ( !defined $pid ) + { + die "fork failed: $!"; + } + if ($pid == 0) + { + exec 'cvs', '-nQq', 'status', '-v', $file; + die "cvs exec failed: $!"; + } + while (<RCS>) { + print OUT; + if (MAIL) { + print MAIL; + } + } + close(RCS); + } +} + +close(OUT); +die "Write to $logfile failed" if $?; + +close(MAIL); +die "Pipe to $mailcmd failed" if $?; + +## must exit cleanly +## +exit 0; diff --git a/gnu/dist/cvs/contrib/log_accum.pl b/gnu/dist/cvs/contrib/log_accum.pl new file mode 100644 index 00000000000..d299fa62b53 --- /dev/null +++ b/gnu/dist/cvs/contrib/log_accum.pl @@ -0,0 +1,571 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# Perl filter to handle the log messages from the checkin of files in +# a directory. This script will group the lists of files by log +# message, and mail a single consolidated log message at the end of +# the commit. +# +# This file assumes a pre-commit checking program that leaves the +# names of the first and last commit directories in a temporary file. +# +# Contributed by David Hampton <hampton@cisco.com> +# +# hacked greatly by Greg A. Woods <woods@planix.com> + +# Usage: log_accum.pl [-d] [-s] [-M module] [[-m mailto] ...] [[-R replyto] ...] [-f logfile] +# -d - turn on debugging +# -m mailto - send mail to "mailto" (multiple) +# -R replyto - set the "Reply-To:" to "replyto" (multiple) +# -M modulename - set module name to "modulename" +# -f logfile - write commit messages to logfile too +# -s - *don't* run "cvs status -v" for each file +# -w - show working directory with log message + +# +# Configurable options +# + +# set this to something that takes a whole message on stdin +$MAILER = "/usr/lib/sendmail -t"; + +# +# End user configurable options. +# + +# Constants (don't change these!) +# +$STATE_NONE = 0; +$STATE_CHANGED = 1; +$STATE_ADDED = 2; +$STATE_REMOVED = 3; +$STATE_LOG = 4; + +$LAST_FILE = "/tmp/#cvs.lastdir"; + +$CHANGED_FILE = "/tmp/#cvs.files.changed"; +$ADDED_FILE = "/tmp/#cvs.files.added"; +$REMOVED_FILE = "/tmp/#cvs.files.removed"; +$LOG_FILE = "/tmp/#cvs.files.log"; + +$FILE_PREFIX = "#cvs.files"; + +# +# Subroutines +# + +sub cleanup_tmpfiles { + local($wd, @files); + + $wd = `pwd`; + chdir("/tmp") || die("Can't chdir('/tmp')\n"); + opendir(DIR, "."); + push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR))); + closedir(DIR); + foreach (@files) { + unlink $_; + } + unlink $LAST_FILE . "." . $id; + + chdir($wd); +} + +sub write_logfile { + local($filename, @lines) = @_; + + open(FILE, ">$filename") || die("Cannot open log file $filename.\n"); + print FILE join("\n", @lines), "\n"; + close(FILE); +} + +sub append_to_logfile { + local($filename, @lines) = @_; + + open(FILE, ">$filename") || die("Cannot open log file $filename.\n"); + print FILE join("\n", @lines), "\n"; + close(FILE); +} + +sub format_names { + local($dir, @files) = @_; + local(@lines); + + $format = "\t%-" . sprintf("%d", length($dir)) . "s%s "; + + $lines[0] = sprintf($format, $dir, ":"); + + if ($debug) { + print STDERR "format_names(): dir = ", $dir, "; files = ", join(":", @files), ".\n"; + } + foreach $file (@files) { + if (length($lines[$#lines]) + length($file) > 65) { + $lines[++$#lines] = sprintf($format, " ", " "); + } + $lines[$#lines] .= $file . " "; + } + + @lines; +} + +sub format_lists { + local(@lines) = @_; + local(@text, @files, $lastdir); + + if ($debug) { + print STDERR "format_lists(): ", join(":", @lines), "\n"; + } + @text = (); + @files = (); + $lastdir = shift @lines; # first thing is always a directory + if ($lastdir !~ /.*\/$/) { + die("Damn, $lastdir doesn't look like a directory!\n"); + } + foreach $line (@lines) { + if ($line =~ /.*\/$/) { + push(@text, &format_names($lastdir, @files)); + $lastdir = $line; + @files = (); + } else { + push(@files, $line); + } + } + push(@text, &format_names($lastdir, @files)); + + @text; +} + +sub append_names_to_file { + local($filename, $dir, @files) = @_; + + if (@files) { + open(FILE, ">>$filename") || die("Cannot open file $filename.\n"); + print FILE $dir, "\n"; + print FILE join("\n", @files), "\n"; + close(FILE); + } +} + +sub read_line { + local($line); + local($filename) = @_; + + open(FILE, "<$filename") || die("Cannot open file $filename.\n"); + $line = <FILE>; + close(FILE); + chop($line); + $line; +} + +sub read_logfile { + local(@text); + local($filename, $leader) = @_; + + open(FILE, "<$filename"); + while (<FILE>) { + chop; + push(@text, $leader.$_); + } + close(FILE); + @text; +} + +sub build_header { + local($header); + local($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + $header = sprintf("CVSROOT:\t%s\nModule name:\t%s\nRepository:\t%s\nChanges by:\t%s@%s\t%02d/%02d/%02d %02d:%02d:%02d", + $cvsroot, + $modulename, + $dir, + $login, $hostdomain, + $year%100, $mon+1, $mday, + $hour, $min, $sec); +} + +sub mail_notification { + local(@text) = @_; + + # if only we had strftime()... stuff stolen from perl's ctime.pl: + local($[) = 0; + + @DoW = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat'); + @MoY = ('Jan','Feb','Mar','Apr','May','Jun', + 'Jul','Aug','Sep','Oct','Nov','Dec'); + + # Determine what time zone is in effect. + # Use GMT if TZ is defined as null, local time if TZ undefined. + # There's no portable way to find the system default timezone. + # + $TZ = defined($ENV{'TZ'}) ? ( $ENV{'TZ'} ? $ENV{'TZ'} : 'GMT' ) : ''; + + # Hack to deal with 'PST8PDT' format of TZ + # Note that this can't deal with all the esoteric forms, but it + # does recognize the most common: [:]STDoff[DST[off][,rule]] + # + if ($TZ =~ /^([^:\d+\-,]{3,})([+-]?\d{1,2}(:\d{1,2}){0,2})([^\d+\-,]{3,})?/) { + $TZ = $isdst ? $4 : $1; + $tzoff = sprintf("%05d", -($2) * 100); + } + + # perl-4.036 doesn't have the $zone or $gmtoff... + ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst, $zone, $gmtoff) = + ($TZ eq 'GMT') ? gmtime(time) : localtime(time); + + $year += ($year < 70) ? 2000 : 1900; + + if ($gmtoff != 0) { + $tzoff = sprintf("%05d", ($gmtoff / 60) * 100); + } + if ($zone ne '') { + $TZ = $zone; + } + + # ok, let's try.... + $rfc822date = sprintf("%s, %2d %s %4d %2d:%02d:%02d %s (%s)", + $DoW[$wday], $mday, $MoY[$mon], $year, + $hour, $min, $sec, $tzoff, $TZ); + + open(MAIL, "| $MAILER"); + print MAIL "Date: " . $rfc822date . "\n"; + print MAIL "Subject: CVS Update: " . $modulename . "\n"; + print MAIL "To: " . $mailto . "\n"; + print MAIL "Reply-To: " . $replyto . "\n"; + print MAIL "\n"; + print MAIL join("\n", @text), "\n"; + close(MAIL); +} + +sub write_commitlog { + local($logfile, @text) = @_; + + open(FILE, ">>$logfile"); + print FILE join("\n", @text), "\n"; + close(FILE); +} + +# +# Main Body +# + +# Initialize basic variables +# +$debug = 0; +$id = getpgrp(); # note, you *must* use a shell which does setpgrp() +$state = $STATE_NONE; +$login = getlogin || (getpwuid($<))[0] || "nobody"; +chop($hostname = `hostname`); +chop($domainname = `domainname`); +if ($domainname !~ '^\..*') { + $domainname = '.' . $domainname; +} +$hostdomain = $hostname . $domainname; +$cvsroot = $ENV{'CVSROOT'}; +$do_status = 1; # moderately useful +$show_wd = 0; # useless in client/server +$modulename = ""; + +# parse command line arguments (file list is seen as one arg) +# +while (@ARGV) { + $arg = shift @ARGV; + + if ($arg eq '-d') { + $debug = 1; + print STDERR "Debug turned on...\n"; + } elsif ($arg eq '-m') { + if ($mailto eq '') { + $mailto = shift @ARGV; + } else { + $mailto = $mailto . ", " . shift @ARGV; + } + } elsif ($arg eq '-R') { + if ($replyto eq '') { + $replyto = shift @ARGV; + } else { + $replyto = $replyto . ", " . shift @ARGV; + } + } elsif ($arg eq '-M') { + $modulename = shift @ARGV; + } elsif ($arg eq '-s') { + $do_status = 0; + } elsif ($arg eq '-w') { + $show_wd = 1; + } elsif ($arg eq '-f') { + ($commitlog) && die("Too many '-f' args\n"); + $commitlog = shift @ARGV; + } else { + ($donefiles) && die("Too many arguments! Check usage.\n"); + $donefiles = 1; + @files = split(/ /, $arg); + } +} +($mailto) || die("No mail recipient specified (use -m)\n"); +if ($replyto eq '') { + $replyto = $login; +} + +# for now, the first "file" is the repository directory being committed, +# relative to the $CVSROOT location +# +@path = split('/', $files[0]); + +# XXX There are some ugly assumptions in here about module names and +# XXX directories relative to the $CVSROOT location -- really should +# XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since +# XXX we have to parse it backwards. +# XXX +# XXX Fortunately it's relatively easy for the user to specify the +# XXX module name as appropriate with a '-M' via the directory +# XXX matching in loginfo. +# +if ($modulename eq "") { + $modulename = $path[0]; # I.e. the module name == top-level dir +} +if ($#path == 0) { + $dir = "."; +} else { + $dir = join('/', @path); +} +$dir = $dir . "/"; + +if ($debug) { + print STDERR "module - ", $modulename, "\n"; + print STDERR "dir - ", $dir, "\n"; + print STDERR "path - ", join(":", @path), "\n"; + print STDERR "files - ", join(":", @files), "\n"; + print STDERR "id - ", $id, "\n"; +} + +# Check for a new directory first. This appears with files set as follows: +# +# files[0] - "path/name/newdir" +# files[1] - "-" +# files[2] - "New" +# files[3] - "directory" +# +if ($files[2] =~ /New/ && $files[3] =~ /directory/) { + local(@text); + + @text = (); + push(@text, &build_header()); + push(@text, ""); + push(@text, $files[0]); + push(@text, ""); + + while (<STDIN>) { + chop; # Drop the newline + push(@text, $_); + } + + &mail_notification($mailto, @text); + + exit 0; +} + +# Check for an import command. This appears with files set as follows: +# +# files[0] - "path/name" +# files[1] - "-" +# files[2] - "Imported" +# files[3] - "sources" +# +if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) { + local(@text); + + @text = (); + push(@text, &build_header()); + push(@text, ""); + push(@text, $files[0]); + push(@text, ""); + + while (<STDIN>) { + chop; # Drop the newline + push(@text, $_); + } + + &mail_notification(@text); + + exit 0; +} + +# Iterate over the body of the message collecting information. +# +while (<STDIN>) { + chop; # Drop the newline + + if (/^In directory/) { + if ($show_wd) { # useless in client/server mode + push(@log_lines, $_); + push(@log_lines, ""); + } + next; + } + + if (/^Modified Files/) { $state = $STATE_CHANGED; next; } + if (/^Added Files/) { $state = $STATE_ADDED; next; } + if (/^Removed Files/) { $state = $STATE_REMOVED; next; } + if (/^Log Message/) { $state = $STATE_LOG; next; } + + s/^[ \t\n]+//; # delete leading whitespace + s/[ \t\n]+$//; # delete trailing whitespace + + if ($state == $STATE_CHANGED) { push(@changed_files, split); } + if ($state == $STATE_ADDED) { push(@added_files, split); } + if ($state == $STATE_REMOVED) { push(@removed_files, split); } + if ($state == $STATE_LOG) { push(@log_lines, $_); } +} + +# Strip leading and trailing blank lines from the log message. Also +# compress multiple blank lines in the body of the message down to a +# single blank line. +# +while ($#log_lines > -1) { + last if ($log_lines[0] ne ""); + shift(@log_lines); +} +while ($#log_lines > -1) { + last if ($log_lines[$#log_lines] ne ""); + pop(@log_lines); +} +for ($i = $#log_lines; $i > 0; $i--) { + if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) { + splice(@log_lines, $i, 1); + } +} + +if ($debug) { + print STDERR "Searching for log file index..."; +} +# Find an index to a log file that matches this log message +# +for ($i = 0; ; $i++) { + local(@text); + + last if (! -e "$LOG_FILE.$i.$id"); # the next available one + @text = &read_logfile("$LOG_FILE.$i.$id", ""); + last if ($#text == -1); # nothing in this file, use it + last if (join(" ", @log_lines) eq join(" ", @text)); # it's the same log message as another +} +if ($debug) { + print STDERR " found log file at $i.$id, now writing tmp files.\n"; +} + +# Spit out the information gathered in this pass. +# +&append_names_to_file("$CHANGED_FILE.$i.$id", $dir, @changed_files); +&append_names_to_file("$ADDED_FILE.$i.$id", $dir, @added_files); +&append_names_to_file("$REMOVED_FILE.$i.$id", $dir, @removed_files); +&write_logfile("$LOG_FILE.$i.$id", @log_lines); + +# Check whether this is the last directory. If not, quit. +# +if ($debug) { + print STDERR "Checking current dir against last dir.\n"; +} +$_ = &read_line("$LAST_FILE.$id"); + +if ($_ ne $cvsroot . "/" . $files[0]) { + if ($debug) { + print STDERR sprintf("Current directory %s is not last directory %s.\n", $cvsroot . "/" .$files[0], $_); + } + exit 0; +} +if ($debug) { + print STDERR sprintf("Current directory %s is last directory %s -- all commits done.\n", $files[0], $_); +} + +# +# End Of Commits! +# + +# This is it. The commits are all finished. Lump everything together +# into a single message, fire a copy off to the mailing list, and drop +# it on the end of the Changes file. +# + +# +# Produce the final compilation of the log messages +# +@text = (); +@status_txt = (); +push(@text, &build_header()); +push(@text, ""); + +for ($i = 0; ; $i++) { + last if (! -e "$LOG_FILE.$i.$id"); # we're done them all! + @lines = &read_logfile("$CHANGED_FILE.$i.$id", ""); + if ($#lines >= 0) { + push(@text, "Modified files:"); + push(@text, &format_lists(@lines)); + } + @lines = &read_logfile("$ADDED_FILE.$i.$id", ""); + if ($#lines >= 0) { + push(@text, "Added files:"); + push(@text, &format_lists(@lines)); + } + @lines = &read_logfile("$REMOVED_FILE.$i.$id", ""); + if ($#lines >= 0) { + push(@text, "Removed files:"); + push(@text, &format_lists(@lines)); + } + if ($#text >= 0) { + push(@text, ""); + } + @lines = &read_logfile("$LOG_FILE.$i.$id", "\t"); + if ($#lines >= 0) { + push(@text, "Log message:"); + push(@text, @lines); + push(@text, ""); + } + if ($do_status) { + local(@changed_files); + + @changed_files = (); + push(@changed_files, &read_logfile("$CHANGED_FILE.$i.$id", "")); + push(@changed_files, &read_logfile("$ADDED_FILE.$i.$id", "")); + push(@changed_files, &read_logfile("$REMOVED_FILE.$i.$id", "")); + + if ($debug) { + print STDERR "main: pre-sort changed_files = ", join(":", @changed_files), ".\n"; + } + sort(@changed_files); + if ($debug) { + print STDERR "main: post-sort changed_files = ", join(":", @changed_files), ".\n"; + } + + foreach $dofile (@changed_files) { + if ($dofile =~ /\/$/) { + next; # ignore the silly "dir" entries + } + if ($debug) { + print STDERR "main(): doing 'cvs -nQq status -v $dofile'\n"; + } + open(STATUS, "-|") || exec 'cvs', '-nQq', 'status', '-v', $dofile; + while (<STATUS>) { + chop; + push(@status_txt, $_); + } + } + } +} + +# Write to the commitlog file +# +if ($commitlog) { + &write_commitlog($commitlog, @text); +} + +if ($#status_txt >= 0) { + push(@text, @status_txt); +} + +# Mailout the notification. +# +&mail_notification(@text); + +# cleanup +# +if (! $debug) { + &cleanup_tmpfiles(); +} + +exit 0; diff --git a/gnu/dist/cvs/contrib/mfpipe.pl b/gnu/dist/cvs/contrib/mfpipe.pl new file mode 100644 index 00000000000..c74d7156490 --- /dev/null +++ b/gnu/dist/cvs/contrib/mfpipe.pl @@ -0,0 +1,85 @@ +#! xPERL_PATHx +# -*-Perl-*- +# +# From: clyne@niwot.scd.ucar.EDU (John Clyne) +# Date: Fri, 28 Feb 92 09:54:21 MST +# +# BTW, i wrote a perl script that is similar to 'nfpipe' except that in +# addition to logging to a file it provides a command line option for mailing +# change notices to a group of users. Obviously you probably wouldn't want +# to mail every change. But there may be certain directories that are commonly +# accessed by a group of users who would benefit from an email notice. +# Especially if they regularly beat on the same directory. Anyway if you +# think anyone would be interested here it is. +# +# File: mfpipe +# +# Author: John Clyne +# National Center for Atmospheric Research +# PO 3000, Boulder, Colorado +# +# Date: Wed Feb 26 18:34:53 MST 1992 +# +# Description: Tee standard input to mail a list of users and to +# a file. Used by CVS logging. +# +# Usage: mfpipe [-f file] [user@host...] +# +# Environment: CVSROOT +# Path to CVS root. +# +# Files: +# +# +# Options: -f file +# Capture output to 'file' +# + +$header = "Log Message:\n"; + +$mailcmd = "| mail -s 'CVS update notice'"; +$whoami = `whoami`; +chop $whoami; +$date = `date`; +chop $date; + +$cvsroot = $ENV{'CVSROOT'}; + +while (@ARGV) { + $arg = shift @ARGV; + + if ($arg eq '-f') { + $file = shift @ARGV; + } + else { + $users = "$users $arg"; + } +} + +if ($users) { + $mailcmd = "$mailcmd $users"; + open(MAIL, $mailcmd) || die "Execing $mail: $!\n"; +} + +if ($file) { + $logfile = "$cvsroot/LOG/$file"; + open(FILE, ">> $logfile") || die "Opening $logfile: $!\n"; +} + +print FILE "$whoami $date--------BEGIN LOG ENTRY-------------\n" if ($logfile); + +while (<>) { + print FILE $log if ($log && $logfile); + + print FILE $_ if ($logfile); + print MAIL $_ if ($users); + + $log = "log: " if ($_ eq $header); +} + +close FILE; +die "Write failed" if $?; +close MAIL; +die "Mail failed" if $?; + +exit 0; diff --git a/gnu/dist/cvs/contrib/rcs-to-cvs.sh b/gnu/dist/cvs/contrib/rcs-to-cvs.sh new file mode 100644 index 00000000000..66a62a9da5e --- /dev/null +++ b/gnu/dist/cvs/contrib/rcs-to-cvs.sh @@ -0,0 +1,184 @@ +#! /bin/sh +# +# Based on the CVS 1.0 checkin csh script. +# Contributed by Per Cederqvist <ceder@signum.se>. +# Rewritten in sh by David MacKenzie <djm@cygnus.com>. +# +# Copyright (c) 1989, Brian Berliner +# +# You may distribute under the terms of the GNU General Public License. +# +############################################################################# +# +# Check in sources that previously were under RCS or no source control system. +# +# The repository is the directory where the sources should be deposited. +# +# Traverses the current directory, ensuring that an +# identical directory structure exists in the repository directory. It +# then checks the files in in the following manner: +# +# 1) If the file doesn't yet exist, check it in as revision 1.1 +# +# The script also is somewhat verbose in letting the user know what is +# going on. It prints a diagnostic when it creates a new file, or updates +# a file that has been modified on the trunk. +# +# Bugs: doesn't put the files in branch 1.1.1 +# doesn't put in release and vendor tags +# +############################################################################# + +usage="Usage: rcs-to-cvs [-v] [-m message] [-f message_file] repository" +vbose=0 +message="" +if [ -d /var/tmp ]; then message_file=/var/tmp/checkin.$$; else message_file=/usr/tmp/checkin.$$; fi +got_one=0 + +if [ $# -lt 1 ]; then + echo "$usage" >&2 + exit 1 +fi + +while [ $# -ne 0 ]; do + case "$1" in + -v) + vbose=1 + ;; + -m) + shift + echo $1 > $message_file + got_one=1 + ;; + -f) + shift + message_file=$1 + got_one=2 + ;; + *) + break + esac + shift +done + +if [ $# -lt 1 ]; then + echo "$usage" >&2 + exit 1 +fi + +repository=$1 +shift + +if [ -z "$CVSROOT" ]; then + echo "Please the environmental variable CVSROOT to the root" >&2 + echo " of the tree you wish to update" >&2 + exit 1 +fi + +if [ $got_one -eq 0 ]; then + echo "Please Edit this file to contain the RCS log information" >$message_file + echo "to be associated with this directory (please remove these lines)">>$message_file + ${EDITOR-vi} $message_file + got_one=1 +fi + +# Ya gotta share. +umask 0 + +update_dir=${CVSROOT}/${repository} +[ ! -d ${update_dir} ] && mkdir $update_dir + +if [ -d SCCS ]; then + echo SCCS files detected! >&2 + exit 1 +fi +if [ -d RCS ]; then + co RCS/* +fi + +for name in * .[a-zA-Z0-9]* +do + case "$name" in + RCS | *~ | \* | .\[a-zA-Z0-9\]\* ) continue ;; + esac + echo $name + if [ $vbose -ne 0 ]; then + echo "Updating ${repository}/${name}" + fi + if [ -d "$name" ]; then + if [ ! -d "${update_dir}/${name}" ]; then + echo "WARNING: Creating new directory ${repository}/${name}" + mkdir "${update_dir}/${name}" + if [ $? -ne 0 ]; then + echo "ERROR: mkdir failed - aborting" >&2 + exit 1 + fi + fi + cd "$name" + if [ $? -ne 0 ]; then + echo "ERROR: Couldn\'t cd to $name - aborting" >&2 + exit 1 + fi + if [ $vbose -ne 0 ]; then + $0 -v -f $message_file "${repository}/${name}" + else + $0 -f $message_file "${repository}/${name}" + fi + if [ $? -ne 0 ]; then + exit 1 + fi + cd .. + else # if not directory + if [ ! -f "$name" ]; then + echo "WARNING: $name is neither a regular file" + echo " nor a directory - ignored" + continue + fi + file="${update_dir}/${name},v" + comment="" + if grep -s '\$Log.*\$' "${name}"; then # If $Log keyword + myext=`echo $name | sed 's,.*\.,,'` + [ "$myext" = "$name" ] && myext= + case "$myext" in + c | csh | e | f | h | l | mac | me | mm | ms | p | r | red | s | sh | sl | cl | ml | el | tex | y | ye | yr | "" ) + ;; + + * ) + echo "For file ${file}:" + grep '\$Log.*\$' "${name}" + echo -n "Please insert a comment leader for file ${name} > " + read comment + ;; + esac + fi + if [ ! -f "$file" ]; then # If not exists in repository + if [ ! -f "${update_dir}/Attic/${name},v" ]; then + echo "WARNING: Creating new file ${repository}/${name}" + if [ -f RCS/"${name}",v ]; then + echo "MSG: Copying old rcs file." + cp RCS/"${name}",v "$file" + else + if [ -n "${comment}" ]; then + rcs -q -i -c"${comment}" -t${message_file} -m'.' "$file" + fi + ci -q -u1.1 -t${message_file} -m'.' "$file" + if [ $? -ne 0 ]; then + echo "ERROR: Initial check-in of $file failed - aborting" >&2 + exit 1 + fi + fi + else + file="${update_dir}/Attic/${name},v" + echo "WARNING: IGNORED: ${repository}/Attic/${name}" + continue + fi + else # File existed + echo "ERROR: File exists in repository: Ignored: $file" + continue + fi + fi +done + +[ $got_one -eq 1 ] && rm -f $message_file + +exit 0 diff --git a/gnu/dist/cvs/contrib/rcs2log.sh b/gnu/dist/cvs/contrib/rcs2log.sh new file mode 100644 index 00000000000..37cfed80a1a --- /dev/null +++ b/gnu/dist/cvs/contrib/rcs2log.sh @@ -0,0 +1,677 @@ +#! /bin/sh + +# RCS to ChangeLog generator + +# Generate a change log prefix from RCS files (perhaps in the CVS repository) +# and the ChangeLog (if any). +# Output the new prefix to standard output. +# You can edit this prefix by hand, and then prepend it to ChangeLog. + +# Ignore log entries that start with `#'. +# Clump together log entries that start with `{topic} ', +# where `topic' contains neither white space nor `}'. + +Help='The default FILEs are the files registered under the working directory. +Options: + + -c CHANGELOG Output a change log prefix to CHANGELOG (default ChangeLog). + -h HOSTNAME Use HOSTNAME in change log entries (default current host). + -i INDENT Indent change log lines by INDENT spaces (default 8). + -l LENGTH Try to limit log lines to LENGTH characters (default 79). + -R If no FILEs are given and RCS is used, recurse through working directory. + -r OPTION Pass OPTION to subsidiary log command. + -t TABWIDTH Tab stops are every TABWIDTH characters (default 8). + -u "LOGIN<tab>FULLNAME<tab>MAILADDR" Assume LOGIN has FULLNAME and MAILADDR. + -v Append RCS revision to file names in log lines. + --help Output help. + --version Output version number. + +Report bugs to <bug-gnu-emacs@gnu.org>.' + +Id='$Id: rcs2log.sh,v 1.1.1.1 2000/09/04 21:51:07 christos Exp $' + +# Copyright 1992, 93, 94, 95, 96, 97, 1998 Free Software Foundation, Inc. + +# 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, 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. + +Copyright='Copyright 1998 Free Software Foundation, Inc. +This program comes with NO WARRANTY, to the extent permitted by law. +You may redistribute copies of this program +under the terms of the GNU General Public License. +For more information about these matters, see the files named COPYING. +Author: Paul Eggert <eggert@twinsun.com>' + +tab=' ' +nl=' +' + +# Parse options. + +# defaults +: ${AWK=awk} +: ${TMPDIR=/tmp} +changelog=ChangeLog # change log file name +datearg= # rlog date option +hostname= # name of local host (if empty, will deduce it later) +indent=8 # indent of log line +length=79 # suggested max width of log line +logins= # login names for people we know fullnames and mailaddrs of +loginFullnameMailaddrs= # login<tab>fullname<tab>mailaddr triplets +logTZ= # time zone for log dates (if empty, use local time) +recursive= # t if we want recursive rlog +revision= # t if we want revision numbers +rlog_options= # options to pass to rlog +tabwidth=8 # width of horizontal tab + +while : +do + case $1 in + -c) changelog=${2?}; shift;; + -i) indent=${2?}; shift;; + -h) hostname=${2?}; shift;; + -l) length=${2?}; shift;; + -[nu]) # -n is obsolescent; it is replaced by -u. + case $1 in + -n) case ${2?}${3?}${4?} in + *"$tab"* | *"$nl"*) + echo >&2 "$0: -n '$2' '$3' '$4': tabs, newlines not allowed" + exit 1 + esac + case $loginFullnameMailaddrs in + '') loginFullnameMailaddrs=$2$tab$3$tab$4;; + ?*) loginFullnameMailaddrs=$loginFullnameMailaddrs$nl$2$tab$3$tab$4 + esac + shift; shift; shift;; + -u) + # If $2 is not tab-separated, use colon for separator. + case ${2?} in + *"$nl"*) + echo >&2 "$0: -u '$2': newlines not allowed" + exit 1;; + *"$tab"*) + t=$tab;; + *) + t=: + esac + case $2 in + *"$t"*"$t"*"$t"*) + echo >&2 "$0: -u '$2': too many fields" + exit 1;; + *"$t"*"$t"*) + ;; + *) + echo >&2 "$0: -u '$2': not enough fields" + exit 1 + esac + case $loginFullnameMailaddrs in + '') loginFullnameMailaddrs=$2;; + ?*) loginFullnameMailaddrs=$loginFullnameMailaddrs$nl$2 + esac + shift + esac + case $logins in + '') logins=$login;; + ?*) logins=$logins$nl$login + esac + ;; + -r) + case $rlog_options in + '') rlog_options=${2?};; + ?*) rlog_options=$rlog_options$nl${2?} + esac + shift;; + -R) recursive=t;; + -t) tabwidth=${2?}; shift;; + -v) revision=t;; + --version) + set $Id + rcs2logVersion=$3 + echo >&2 "rcs2log (GNU Emacs) $rcs2logVersion$nl$Copyright" + exit 0;; + -*) echo >&2 "Usage: $0 [OPTION]... [FILE ...]$nl$Help" + case $1 in + --help) exit 0;; + *) exit 1 + esac;; + *) break + esac + shift +done + +month_data=' + m[0]="Jan"; m[1]="Feb"; m[2]="Mar" + m[3]="Apr"; m[4]="May"; m[5]="Jun" + m[6]="Jul"; m[7]="Aug"; m[8]="Sep" + m[9]="Oct"; m[10]="Nov"; m[11]="Dec" +' + + +# Put rlog output into $rlogout. + +# If no rlog options are given, +# log the revisions checked in since the first ChangeLog entry. +# Since ChangeLog is only by date, some of these revisions may be duplicates of +# what's already in ChangeLog; it's the user's responsibility to remove them. +case $rlog_options in +'') + if test -s "$changelog" + then + e=' + /^[0-9]+-[0-9][0-9]-[0-9][0-9]/{ + # ISO 8601 date + print $1 + exit + } + /^... ... [ 0-9][0-9] [ 0-9][0-9]:[0-9][0-9]:[0-9][0-9] [0-9]+ /{ + # old-fashioned date and time (Emacs 19.31 and earlier) + '"$month_data"' + year = $5 + for (i=0; i<=11; i++) if (m[i] == $2) break + dd = $3 + printf "%d-%02d-%02d\n", year, i+1, dd + exit + } + ' + d=`$AWK "$e" <"$changelog"` || exit + case $d in + ?*) datearg="-d>$d" + esac + fi +esac + +# Use TZ specified by ChangeLog local variable, if any. +if test -s "$changelog" +then + extractTZ=' + /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*"\([^"]*\)".*/{ + s//\1/; p; q + } + /^.*change-log-time-zone-rule['"$tab"' ]*:['"$tab"' ]*t.*/{ + s//UTC0/; p; q + } + ' + logTZ=`tail "$changelog" | sed -n "$extractTZ"` + case $logTZ in + ?*) TZ=$logTZ; export TZ + esac +fi + +# If CVS is in use, examine its repository, not the normal RCS files. +if test ! -f CVS/Repository +then + rlog=rlog + repository= +else + rlog='cvs -q log' + repository=`sed 1q <CVS/Repository` || exit + test ! -f CVS/Root || CVSROOT=`cat <CVS/Root` || exit + case $CVSROOT in + *:/*) + # remote repository + ;; + *) + # local repository + case $repository in + /*) ;; + *) repository=${CVSROOT?}/$repository + esac + if test ! -d "$repository" + then + echo >&2 "$0: $repository: bad repository (see CVS/Repository)" + exit 1 + fi + esac +fi + +# Use $rlog's -zLT option, if $rlog supports it. +case `$rlog -zLT 2>&1` in +*' option'*) ;; +*) + case $rlog_options in + '') rlog_options=-zLT;; + ?*) rlog_options=-zLT$nl$rlog_options + esac +esac + +# With no arguments, examine all files under the RCS directory. +case $# in +0) + case $repository in + '') + oldIFS=$IFS + IFS=$nl + case $recursive in + t) + RCSdirs=`find . -name RCS -type d -print` + filesFromRCSfiles='s|,v$||; s|/RCS/|/|; s|^\./||' + files=` + { + case $RCSdirs in + ?*) find $RCSdirs \ + -type f \ + ! -name '*_' \ + ! -name ',*,' \ + ! -name '.*_' \ + ! -name .rcsfreeze.log \ + ! -name .rcsfreeze.ver \ + -print + esac + find . -name '*,v' -print + } | + sort -u | + sed "$filesFromRCSfiles" + `;; + *) + files= + for file in RCS/.* RCS/* .*,v *,v + do + case $file in + RCS/. | RCS/.. | RCS/,*, | RCS/*_) continue;; + RCS/.rcsfreeze.log | RCS/.rcsfreeze.ver) continue;; + RCS/.\* | RCS/\* | .\*,v | \*,v) test -f "$file" || continue;; + RCS/*,v | RCS/.*,v) ;; + RCS/* | RCS/.*) test -f "$file" || continue + esac + case $files in + '') files=$file;; + ?*) files=$files$nl$file + esac + done + case $files in + '') exit 0 + esac + esac + set x $files + shift + IFS=$oldIFS + esac +esac + +llogout=$TMPDIR/rcs2log$$l +rlogout=$TMPDIR/rcs2log$$r +trap exit 1 2 13 15 +trap "rm -f $llogout $rlogout; exit 1" 0 + +case $datearg in +?*) $rlog $rlog_options "$datearg" ${1+"$@"} >$rlogout;; +'') $rlog $rlog_options ${1+"$@"} >$rlogout +esac || exit + + +# Get the full name of each author the logs mention, and set initialize_fullname +# to awk code that initializes the `fullname' awk associative array. +# Warning: foreign authors (i.e. not known in the passwd file) are mishandled; +# you have to fix the resulting output by hand. + +initialize_fullname= +initialize_mailaddr= + +case $loginFullnameMailaddrs in +?*) + case $loginFullnameMailaddrs in + *\"* | *\\*) + sed 's/["\\]/\\&/g' >$llogout <<EOF || exit +$loginFullnameMailaddrs +EOF + loginFullnameMailaddrs=`cat $llogout` + esac + + oldIFS=$IFS + IFS=$nl + for loginFullnameMailaddr in $loginFullnameMailaddrs + do + case $loginFullnameMailaddr in + *"$tab"*) IFS=$tab;; + *) IFS=: + esac + set x $loginFullnameMailaddr + login=$2 + fullname=$3 + mailaddr=$4 + initialize_fullname="$initialize_fullname + fullname[\"$login\"] = \"$fullname\"" + initialize_mailaddr="$initialize_mailaddr + mailaddr[\"$login\"] = \"$mailaddr\"" + done + IFS=$oldIFS +esac + +case $llogout in +?*) sort -u -o $llogout <<EOF || exit +$logins +EOF +esac +output_authors='/^date: / { + 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 ~ /^[^;]*;$/) { + print substr($5, 1, length($5)-1) + } +}' +authors=` + $AWK "$output_authors" <$rlogout | + case $llogout in + '') sort -u;; + ?*) sort -u | comm -23 - $llogout + esac +` +case $authors in +?*) + cat >$llogout <<EOF || exit +$authors +EOF + initialize_author_script='s/["\\]/\\&/g; s/.*/author[\"&\"] = 1/' + initialize_author=`sed -e "$initialize_author_script" <$llogout` + awkscript=' + BEGIN { + alphabet = "abcdefghijklmnopqrstuvwxyz" + ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + '"$initialize_author"' + } + { + if (author[$1]) { + fullname = $5 + if (fullname ~ /[0-9]+-[^(]*\([0-9]+\)$/) { + # Remove the junk from fullnames like "0000-Admin(0000)". + fullname = substr(fullname, index(fullname, "-") + 1) + fullname = substr(fullname, 1, index(fullname, "(") - 1) + } + if (fullname ~ /,[^ ]/) { + # Some sites put comma-separated junk after the fullname. + # Remove it, but leave "Bill Gates, Jr" alone. + fullname = substr(fullname, 1, index(fullname, ",") - 1) + } + abbr = index(fullname, "&") + if (abbr) { + a = substr($1, 1, 1) + A = a + i = index(alphabet, a) + if (i) A = substr(ALPHABET, i, 1) + fullname = substr(fullname, 1, abbr-1) A substr($1, 2) substr(fullname, abbr+1) + } + + # Quote quotes and backslashes properly in full names. + # Do not use gsub; traditional awk lacks it. + quoted = "" + rest = fullname + for (;;) { + p = index(rest, "\\") + q = index(rest, "\"") + if (p) { + if (q && q<p) p = q + } else { + if (!q) break + p = q + } + quoted = quoted substr(rest, 1, p-1) "\\" substr(rest, p, 1) + rest = substr(rest, p+1) + } + + printf "fullname[\"%s\"] = \"%s%s\"\n", $1, quoted, rest + author[$1] = 0 + } + } + ' + + initialize_fullname=` + { + (getent passwd $authors) || + ( + cat /etc/passwd + for author in $authors + do NIS_PATH= nismatch $author passwd.org_dir + done + ypmatch $authors passwd + ) + } 2>/dev/null | + $AWK -F: "$awkscript" + `$initialize_fullname +esac + + +# Function to print a single log line. +# We don't use awk functions, to stay compatible with old awk versions. +# `Log' is the log message (with \n replaced by \001). +# `files' contains the affected files. +printlogline='{ + + # Following the GNU coding standards, rewrite + # * file: (function): comment + # to + # * file (function): comment + if (Log ~ /^\([^)]*\): /) { + i = index(Log, ")") + files = files " " substr(Log, 1, i) + Log = substr(Log, i+3) + } + + # If "label: comment" is too long, break the line after the ":". + sep = " " + if ('"$length"' <= '"$indent"' + 1 + length(files) + index(Log, SOH)) sep = "\n" indent_string + + # Print the label. + printf "%s*%s:", indent_string, files + + # Print each line of the log, transliterating \001 to \n. + while ((i = index(Log, SOH)) != 0) { + logline = substr(Log, 1, i-1) + if (logline ~ /[^'"$tab"' ]/) { + printf "%s%s\n", sep, logline + } else { + print "" + } + sep = indent_string + Log = substr(Log, i+1) + } +}' + +# Pattern to match the `revision' line of rlog output. +rlog_revision_pattern='^revision [0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)*(['"$tab"' ]+locked by: [^'"$tab"' $,.0-9:;@]*[^'"$tab"' $,:;@][^'"$tab"' $,.0-9:;@]*;)?['"$tab"' ]*$' + +case $hostname in +'') + hostname=`( + hostname || uname -n || uuname -l || cat /etc/whoami + ) 2>/dev/null` || { + echo >&2 "$0: cannot deduce hostname" + exit 1 + } + + case $hostname in + *.*) ;; + *) + domainname=`(domainname) 2>/dev/null` && + case $domainname in + *.*) hostname=$hostname.$domainname + esac + esac +esac + + +# Process the rlog output, generating ChangeLog style entries. + +# First, reformat the rlog output so that each line contains one log entry. +# Transliterate \n to \001 so that multiline entries fit on a single line. +# Discard irrelevant rlog output. +$AWK <$rlogout ' + BEGIN { repository = "'"$repository"'" } + /^RCS file:/ { + if (repository != "") { + filename = $3 + if (substr(filename, 1, length(repository) + 1) == repository "/") { + filename = substr(filename, length(repository) + 2) + } + if (filename ~ /,v$/) { + filename = substr(filename, 1, length(filename) - 2) + } + if (filename ~ /(^|\/)Attic\/[^\/]*$/) { + i = length(filename) + while (substr(filename, i, 1) != "/") i-- + filename = substr(filename, 1, i - 6) substr(filename, i + 1) + } + } + rev = "?" + } + /^Working file:/ { if (repository == "") filename = $3 } + /'"$rlog_revision_pattern"'/, /^(-----------*|===========*)$/ { + line = $0 + if (line ~ /'"$rlog_revision_pattern"'/) { + rev = $2 + next + } + if (line ~ /^date: [0-9][- +\/0-9:]*;/) { + date = $2 + if (date ~ /\//) { + # This is a traditional RCS format date YYYY/MM/DD. + # Replace "/"s with "-"s to get ISO format. + newdate = "" + while ((i = index(date, "/")) != 0) { + newdate = newdate substr(date, 1, i-1) "-" + date = substr(date, i+1) + } + date = newdate date + } + time = substr($3, 1, length($3) - 1) + author = substr($5, 1, length($5)-1) + printf "%s %s %s %s %s %c", filename, rev, date, time, author, 1 + rev = "?" + next + } + if (line ~ /^branches: /) { next } + if (line ~ /^(-----------*|===========*)$/) { print ""; next } + if (line == "Initial revision" || line ~ /^file .+ was initially added on branch .+\.$/) { + line = "New file." + } + printf "%s%c", line, 1 + } +' | + +# Now each line is of the form +# FILENAME REVISION YYYY-MM-DD HH:MM:SS[+-TIMEZONE] AUTHOR \001LOG +# where \001 stands for a carriage return, +# and each line of the log is terminated by \001 instead of \n. +# Sort the log entries, first by date+time (in reverse order), +# then by author, then by log entry, and finally by file name and revision +# (just in case). +sort +2 -4r +4 +0 | + +# Finally, reformat the sorted log entries. +$AWK ' + BEGIN { + logTZ = "'"$logTZ"'" + revision = "'"$revision"'" + + # Some awk variants do not understand "\001", so we have to + # put the char directly in the file. + SOH="" # <-- There is a single SOH (octal code 001) here. + + # Initialize the fullname and mailaddr associative arrays. + '"$initialize_fullname"' + '"$initialize_mailaddr"' + + # Initialize indent string. + indent_string = "" + i = '"$indent"' + if (0 < '"$tabwidth"') + for (; '"$tabwidth"' <= i; i -= '"$tabwidth"') + indent_string = indent_string "\t" + while (1 <= i--) + indent_string = indent_string " " + } + + { + newlog = substr($0, 1 + index($0, SOH)) + + # Ignore log entries prefixed by "#". + if (newlog ~ /^#/) { next } + + if (Log != newlog || date != $3 || author != $5) { + + # The previous log and this log differ. + + # Print the old log. + if (date != "") '"$printlogline"' + + # Logs that begin with "{clumpname} " should be grouped together, + # and the clumpname should be removed. + # Extract the new clumpname from the log header, + # and use it to decide whether to output a blank line. + newclumpname = "" + sep = "\n" + if (date == "") sep = "" + if (newlog ~ /^\{[^'"$tab"' }]*}['"$tab"' ]/) { + i = index(newlog, "}") + newclumpname = substr(newlog, 1, i) + while (substr(newlog, i+1) ~ /^['"$tab"' ]/) i++ + newlog = substr(newlog, i+1) + if (clumpname == newclumpname) sep = "" + } + printf sep + clumpname = newclumpname + + # Get ready for the next log. + Log = newlog + if (files != "") + for (i in filesknown) + filesknown[i] = 0 + files = "" + } + if (date != $3 || author != $5) { + # The previous date+author and this date+author differ. + # Print the new one. + date = $3 + time = $4 + author = $5 + + zone = "" + if (logTZ && ((i = index(time, "-")) || (i = index(time, "+")))) + zone = " " substr(time, i) + + # Print "date[ timezone] fullname <email address>". + # Get fullname and email address from associative arrays; + # default to author and author@hostname if not in arrays. + if (fullname[author]) + auth = fullname[author] + else + auth = author + printf "%s%s %s ", date, zone, auth + if (mailaddr[author]) + printf "<%s>\n\n", mailaddr[author] + else + printf "<%s@%s>\n\n", author, "'"$hostname"'" + } + if (! filesknown[$1]) { + filesknown[$1] = 1 + if (files == "") files = " " $1 + else files = files ", " $1 + if (revision && $2 != "?") files = files " " $2 + } + } + END { + # Print the last log. + if (date != "") { + '"$printlogline"' + printf "\n" + } + } +' && + + +# Exit successfully. + +exec rm -f $llogout $rlogout + +# Local Variables: +# tab-width:4 +# End: diff --git a/gnu/dist/cvs/contrib/rcs2sccs.sh b/gnu/dist/cvs/contrib/rcs2sccs.sh new file mode 100644 index 00000000000..b089dfef065 --- /dev/null +++ b/gnu/dist/cvs/contrib/rcs2sccs.sh @@ -0,0 +1,141 @@ +#! /bin/sh +# +# + +############################################################ +# Error checking +# +if [ ! -d SCCS ] ; then + mkdir SCCS +fi + +logfile=/tmp/rcs2sccs_$$_log +rm -f $logfile +tmpfile=/tmp/rcs2sccs_$$_tmp +rm -f $tmpfile +emptyfile=/tmp/rcs2sccs_$$_empty +echo -n "" > $emptyfile +initialfile=/tmp/rcs2sccs_$$_init +echo "Initial revision" > $initialfile +sedfile=/tmp/rcs2sccs_$$_sed +rm -f $sedfile +revfile=/tmp/rcs2sccs_$$_rev +rm -f $revfile +commentfile=/tmp/rcs2sccs_$$_comment +rm -f $commentfile + +# create the sed script +cat > $sedfile << EOF +s,;Id;,%Z%%M% %I% %E%,g +s,;SunId;,%Z%%M% %I% %E%,g +s,;RCSfile;,%M%,g +s,;Revision;,%I%,g +s,;Date;,%E%,g +s,;Id:.*;,%Z%%M% %I% %E%,g +s,;SunId:.*;,%Z%%M% %I% %E%,g +s,;RCSfile:.*;,%M%,g +s,;Revision:.*;,%I%,g +s,;Date:.*;,%E%,g +EOF +sed -e 's/;/\\$/g' $sedfile > $tmpfile +cp $tmpfile $sedfile +############################################################ +# Loop over every RCS file in RCS dir +# +for vfile in *,v; do + # get rid of the ",v" at the end of the name + file=`echo $vfile | sed -e 's/,v$//'` + + # work on each rev of that file in ascending order + firsttime=1 + rlog $file | grep "^revision [0-9][0-9]*\." | awk '{print $2}' | sed -e 's/\./ /g' | sort -n -u +0 +1 +2 +3 +4 +5 +6 +7 +8 | sed -e 's/ /./g' > $revfile + for rev in `cat $revfile`; do + if [ $? != 0 ]; then + echo ERROR - revision + exit + fi + # get file into current dir and get stats + date=`rlog -r$rev $file | grep "^date: " | awk '{print $2; exit}' | sed -e 's/^19//'` + time=`rlog -r$rev $file | grep "^date: " | awk '{print $3; exit}' | sed -e 's/;//'` + author=`rlog -r$rev $file | grep "^date: " | awk '{print $5; exit}' | sed -e 's/;//'` + date="$date $time" + echo "" + rlog -r$rev $file | sed -e '/^branches: /d' -e '1,/^date: /d' -e '/^===========/d' -e 's/$/\\/' | awk '{if ((total += length($0) + 1) < 510) print $0}' > $commentfile + echo "==> file $file, rev=$rev, date=$date, author=$author" + rm -f $file + co -r$rev $file >> $logfile 2>&1 + if [ $? != 0 ]; then + echo ERROR - co + exit + fi + echo checked out of RCS + + # add SCCS keywords in place of RCS keywords + sed -f $sedfile $file > $tmpfile + if [ $? != 0 ]; then + echo ERROR - sed + exit + fi + echo performed keyword substitutions + rm -f $file + cp $tmpfile $file + + # check file into SCCS + if [ "$firsttime" = "1" ]; then + firsttime=0 + echo about to do sccs admin + echo sccs admin -n -i$file $file < $commentfile + sccs admin -n -i$file $file < $commentfile >> $logfile 2>&1 + if [ $? != 0 ]; then + echo ERROR - sccs admin + exit + fi + echo initial rev checked into SCCS + else + case $rev in + *.*.*.*) + brev=`echo $rev | sed -e 's/\.[0-9]*$//'` + sccs admin -fb $file 2>>$logfile + echo sccs get -e -p -r$brev $file + sccs get -e -p -r$brev $file >/dev/null 2>>$logfile + ;; + *) + echo sccs get -e -p $file + sccs get -e -p $file >/dev/null 2>> $logfile + ;; + esac + if [ $? != 0 ]; then + echo ERROR - sccs get + exit + fi + sccs delta $file < $commentfile >> $logfile 2>&1 + if [ $? != 0 ]; then + echo ERROR - sccs delta -r$rev $file + exit + fi + echo checked into SCCS + fi + sed -e "s;^d D $rev ../../.. ..:..:.. [^ ][^ ]*;d D $rev $date $author;" SCCS/s.$file > $tmpfile + rm -f SCCS/s.$file + cp $tmpfile SCCS/s.$file + chmod 444 SCCS/s.$file + sccs admin -z $file + if [ $? != 0 ]; then + echo ERROR - sccs admin -z + exit + fi + done + rm -f $file +done + + +############################################################ +# Clean up +# +echo cleaning up... +rm -f $tmpfile $emptyfile $initialfile $sedfile $commentfile +echo =================================================== +echo " Conversion Completed Successfully" +echo =================================================== + +rm -f *,v diff --git a/gnu/dist/cvs/contrib/rcslock.pl b/gnu/dist/cvs/contrib/rcslock.pl new file mode 100644 index 00000000000..01e349ff025 --- /dev/null +++ b/gnu/dist/cvs/contrib/rcslock.pl @@ -0,0 +1,235 @@ +#! xPERL_PATHx +# -*-Perl-*- + +# Author: John Rouillard (rouilj@cs.umb.edu) +# Supported: Yeah right. (Well what do you expect for 2 hours work?) +# Blame-to: rouilj@cs.umb.edu +# Complaints to: Anybody except Brian Berliner, he's blameless for +# this script. +# Acknowlegements: The base code for this script has been acquired +# from the log.pl script. + +# rcslock.pl - A program to prevent commits when a file to be ckecked +# in is locked in the repository. + +# There are times when you need exclusive access to a file. This +# often occurs when binaries are checked into the repository, since +# cvs's (actually rcs's) text based merging mechanism won't work. This +# script allows you to use the rcs lock mechanism (rcs -l) to make +# sure that no changes to a repository are able to be committed if +# those changes would result in a locked file being changed. + +# WARNING: +# This script will work only if locking is set to strict. +# + +# Setup: +# Add the following line to the commitinfo file: + +# ALL /local/location/for/script/lockcheck [options] + +# Where ALL is replaced by any suitable regular expression. +# Options are -v for verbose info, or -d for debugging info. +# The %s will provide the repository directory name and the names of +# all changed files. + +# Use: +# When a developer needs exclusive access to a version of a file, s/he +# should use "rcs -l" in the repository tree to lock the version they +# are working on. CVS will automagically release the lock when the +# commit is performed. + +# Method: +# An "rlog -h" is exec'ed to give info on all about to be +# committed files. This (header) information is parsed to determine +# if any locks are outstanding and what versions of the file are +# locked. This filename, version number info is used to index an +# associative array. All of the files to be committed are checked to +# see if any locks are outstanding. If locks are outstanding, the +# version number of the current file (taken from the CVS/Entries +# subdirectory) is used in the key to determine if that version is +# locked. If the file being checked in is locked by the person doing +# the checkin, the commit is allowed, but if the lock is held on that +# version of a file by another person, the commit is not allowed. + +$ext = ",v"; # The extension on your rcs files. + +$\="\n"; # I hate having to put \n's at the end of my print statements +$,=' '; # Spaces should occur between arguments to print when printed + +# turn off setgid +# +$) = $(; + +# +# parse command line arguments +# +require 'getopts.pl'; + +&Getopts("vd"); # verbose or debugging + +# Verbose is useful when debugging +$opt_v = $opt_d if defined $opt_d; + +# $files[0] is really the name of the subdirectory. +# @files = split(/ /,$ARGV[0]); +@files = @ARGV[0..$#ARGV]; +$cvsroot = $ENV{'CVSROOT'}; + +# +# get login name +# +$login = getlogin || (getpwuid($<))[0] || "nobody"; + +# +# save the current directory since we have to return here to parse the +# CVS/Entries file if a lock is found. +# +$pwd = `/bin/pwd`; +chop $pwd; + +print "Starting directory is $pwd" if defined $opt_d ; + +# +# cd to the repository directory and check on the files. +# +print "Checking directory ", $files[0] if defined $opt_v ; + +if ( $files[0] =~ /^\// ) +{ + print "Directory path is $files[0]" if defined $opt_d ; + chdir $files[0] || die "Can't change to repository directory $files[0]" ; +} +else +{ + print "Directory path is $cvsroot/$files[0]" if defined $opt_d ; + chdir ($cvsroot . "/" . $files[0]) || + die "Can't change to repository directory $files[0] in $cvsroot" ; +} + + +# Open the rlog process and apss all of the file names to that one +# process to cut down on exec overhead. This may backfire if there +# are too many files for the system buffer to handle, but if there are +# that many files, chances are that the cvs repository is not set up +# cleanly. + +print "opening rlog -h @files[1..$#files] |" if defined $opt_d; + +open( RLOG, "rlog -h @files[1..$#files] |") || die "Can't run rlog command" ; + +# Create the locks associative array. The elements in the array are +# of two types: +# +# The name of the RCS file with a value of the total number of locks found +# for that file, +# or +# +# The name of the rcs file concatenated with the version number of the lock. +# The value of this element is the name of the locker. + +# The regular expressions used to split the rcs info may have to be changed. +# The current ones work for rcs 5.6. + +$lock = 0; + +while (<RLOG>) +{ + chop; + next if /^$/; # ditch blank lines + + if ( $_ =~ /^RCS file: (.*)$/ ) + { + $curfile = $1; + next; + } + + if ( $_ =~ /^locks: strict$/ ) + { + $lock = 1 ; + next; + } + + if ( $lock ) + { + # access list: is the line immediately following the list of locks. + if ( /^access list:/ ) + { # we are done getting lock info for this file. + $lock = 0; + } + else + { # We are accumulating lock info. + + # increment the lock count + $locks{$curfile}++; + # save the info on the version that is locked. $2 is the + # version number $1 is the name of the locker. + $locks{"$curfile" . "$2"} = $1 + if /[ ]*([a-zA-Z._]*): ([0-9.]*)$/; + + print "lock by $1 found on $curfile version $2" if defined $opt_d; + + } + } +} + +# Lets go back to the starting directory and see if any locked files +# are ones we are interested in. + +chdir $pwd; + +# fo all of the file names (remember $files[0] is the directory name +foreach $i (@files[1..$#files]) +{ + if ( defined $locks{$i . $ext} ) + { # well the file has at least one lock outstanding + + # find the base version number of our file + &parse_cvs_entry($i,*entry); + + # is our version of this file locked? + if ( defined $locks{$i . $ext . $entry{"version"}} ) + { # if so, it is by us? + if ( $login ne ($by = $locks{$i . $ext . $entry{"version"}}) ) + {# crud somebody else has it locked. + $outstanding_lock++ ; + print "$by has file $i locked for version " , $entry{"version"}; + } + else + { # yeah I have it locked. + print "You have a lock on file $i for version " , $entry{"version"} + if defined $opt_v; + } + } + } +} + +exit $outstanding_lock; + + +### End of main program + +sub parse_cvs_entry +{ # a very simple minded hack at parsing an entries file. +local ( $file, *entry ) = @_; +local ( @pp ); + + +open(ENTRIES, "< CVS/Entries") || die "Can't open entries file"; + +while (<ENTRIES>) + { + if ( $_ =~ /^\/$file\// ) + { + @pp = split('/'); + + $entry{"name"} = $pp[1]; + $entry{"version"} = $pp[2]; + $entry{"dates"} = $pp[3]; + $entry{"name"} = $pp[4]; + $entry{"name"} = $pp[5]; + $entry{"sticky"} = $pp[6]; + return; + } + } +} diff --git a/gnu/dist/cvs/contrib/sccs2rcs.csh b/gnu/dist/cvs/contrib/sccs2rcs.csh new file mode 100644 index 00000000000..a1dea01b19e --- /dev/null +++ b/gnu/dist/cvs/contrib/sccs2rcs.csh @@ -0,0 +1,275 @@ +#! xCSH_PATHx -f +# +# Sccs2rcs is a script to convert an existing SCCS +# history into an RCS history without losing any of +# the information contained therein. +# It has been tested under the following OS's: +# SunOS 3.5, 4.0.3, 4.1 +# Ultrix-32 2.0, 3.1 +# +# Things to note: +# + It will NOT delete or alter your ./SCCS history under any circumstances. +# +# + Run in a directory where ./SCCS exists and where you can +# create ./RCS +# +# + /usr/local/bin is put in front of the default path. +# (SCCS under Ultrix is set-uid sccs, bad bad bad, so +# /usr/local/bin/sccs here fixes that) +# +# + Date, time, author, comments, branches, are all preserved. +# +# + If a command fails somewhere in the middle, it bombs with +# a message -- remove what it's done so far and try again. +# "rm -rf RCS; sccs unedit `sccs tell`; sccs clean" +# There is no recovery and exit is far from graceful. +# If a particular module is hanging you up, consider +# doing it separately; move it from the current area so that +# the next run will have a better chance or working. +# Also (for the brave only) you might consider hacking +# the s-file for simpler problems: I've successfully changed +# the date of a delta to be in sync, then run "sccs admin -z" +# on the thing. +# +# + After everything finishes, ./SCCS will be moved to ./old-SCCS. +# +# This file may be copied, processed, hacked, mutilated, and +# even destroyed as long as you don't tell anyone you wrote it. +# +# Ken Cox +# Viewlogic Systems, Inc. +# kenstir@viewlogic.com +# ...!harvard!cg-atla!viewlog!kenstir +# +# Various hacks made by Brian Berliner before inclusion in CVS contrib area. + + +#we'll assume the user set up the path correctly +# for the Pmax, /usr/ucb/sccs is suid sccs, what a pain +# /usr/local/bin/sccs should override /usr/ucb/sccs there +set path = (/usr/local/bin $path) + + +############################################################ +# Error checking +# +if (! -w .) then + echo "Error: ./ not writeable by you." + exit 1 +endif +if (! -d SCCS) then + echo "Error: ./SCCS directory not found." + exit 1 +endif +set edits = (`sccs tell`) +if ($#edits) then + echo "Error: $#edits file(s) out for edit...clean up before converting." + exit 1 +endif +if (-d RCS) then + echo "Warning: RCS directory exists" + if (`ls -a RCS | wc -l` > 2) then + echo "Error: RCS directory not empty + exit 1 + endif +else + mkdir RCS +endif + +sccs clean + +set logfile = /tmp/sccs2rcs_$$_log +rm -f $logfile +set tmpfile = /tmp/sccs2rcs_$$_tmp +rm -f $tmpfile +set emptyfile = /tmp/sccs2rcs_$$_empty +echo -n "" > $emptyfile +set initialfile = /tmp/sccs2rcs_$$_init +echo "Initial revision" > $initialfile +set sedfile = /tmp/sccs2rcs_$$_sed +rm -f $sedfile +set revfile = /tmp/sccs2rcs_$$_rev +rm -f $revfile + +# the quotes surround the dollar signs to fool RCS when I check in this script +set sccs_keywords = (\ + '%W%[ ]*%G%'\ + '%W%[ ]*%E%'\ + '%W%'\ + '%Z%%M%[ ]*%I%[ ]*%G%'\ + '%Z%%M%[ ]*%I%[ ]*%E%'\ + '%M%[ ]*%I%[ ]*%G%'\ + '%M%[ ]*%I%[ ]*%E%'\ + '%M%'\ + '%I%'\ + '%G%'\ + '%E%'\ + '%U%') +set rcs_keywords = (\ + '$'Id'$'\ + '$'Id'$'\ + '$'Id'$'\ + '$'SunId'$'\ + '$'SunId'$'\ + '$'Id'$'\ + '$'Id'$'\ + '$'RCSfile'$'\ + '$'Revision'$'\ + '$'Date'$'\ + '$'Date'$'\ + '') + + +############################################################ +# Get some answers from user +# +echo "" +echo "Do you want to be prompted for a description of each" +echo "file as it is checked in to RCS initially?" +echo -n "(y=prompt for description, n=null description) [y] ?" +set ans = $< +if ((_$ans == _) || (_$ans == _y) || (_$ans == _Y)) then + set nodesc = 0 +else + set nodesc = 1 +endif +echo "" +echo "The default keyword substitutions are as follows and are" +echo "applied in the order specified:" +set i = 1 +while ($i <= $#sccs_keywords) +# echo ' '\"$sccs_keywords[$i]\"' ==> '\"$rcs_keywords[$i]\" + echo " $sccs_keywords[$i] ==> $rcs_keywords[$i]" + @ i = $i + 1 +end +echo "" +echo -n "Do you want to change them [n] ?" +set ans = $< +if ((_$ans != _) && (_$ans != _n) && (_$ans != _N)) then + echo "You can't always get what you want." + echo "Edit this script file and change the variables:" + echo ' $sccs_keywords' + echo ' $rcs_keywords' +else + echo "good idea." +endif + +# create the sed script +set i = 1 +while ($i <= $#sccs_keywords) + echo "s,$sccs_keywords[$i],$rcs_keywords[$i],g" >> $sedfile + @ i = $i + 1 +end + +onintr ERROR + +############################################################ +# Loop over every s-file in SCCS dir +# +foreach sfile (SCCS/s.*) + # get rid of the "s." at the beginning of the name + set file = `echo $sfile:t | sed -e "s/^..//"` + + # work on each rev of that file in ascending order + set firsttime = 1 + sccs prs $file | grep "^D " | awk '{print $2}' | sed -e 's/\./ /g' | sort -n -u +0 +1 +2 +3 +4 +5 +6 +7 +8 | sed -e 's/ /./g' > $revfile + foreach rev (`cat $revfile`) + if ($status != 0) goto ERROR + + # get file into current dir and get stats + set date = `sccs prs -r$rev $file | grep "^D " | awk '{printf("19%s %s", $3, $4); exit}'` + set author = `sccs prs -r$rev $file | grep "^D " | awk '{print $5; exit}'` + echo "" + echo "==> file $file, rev=$rev, date=$date, author=$author" + sccs edit -r$rev $file >>& $logfile + if ($status != 0) goto ERROR + echo checked out of SCCS + + # add RCS keywords in place of SCCS keywords + sed -f $sedfile $file > $tmpfile + if ($status != 0) goto ERROR + echo performed keyword substitutions + cp $tmpfile $file + + # check file into RCS + if ($firsttime) then + set firsttime = 0 + if ($nodesc) then + echo about to do ci + echo ci -f -r$rev -d"$date" -w$author -t$emptyfile $file + ci -f -r$rev -d"$date" -w$author -t$emptyfile $file < $initialfile >>& $logfile + if ($status != 0) goto ERROR + echo initial rev checked into RCS without description + else + echo "" + echo Enter a brief description of the file $file \(end w/ Ctrl-D\): + cat > $tmpfile + ci -f -r$rev -d"$date" -w$author -t$tmpfile $file < $initialfile >>& $logfile + if ($status != 0) goto ERROR + echo initial rev checked into RCS + endif + else + # get RCS lock + set lckrev = `echo $rev | sed -e 's/\.[0-9]*$//'` + if ("$lckrev" =~ [0-9]*.*) then + # need to lock the brach -- it is OK if the lock fails + rcs -l$lckrev $file >>& $logfile + else + # need to lock the trunk -- must succeed + rcs -l $file >>& $logfile + if ($status != 0) goto ERROR + endif + echo got lock + sccs prs -r$rev $file | grep "." > $tmpfile + # it's OK if grep fails here and gives status == 1 + # put the delta message in $tmpfile + ed $tmpfile >>& $logfile <<EOF +/COMMENTS +1,.d +w +q +EOF + ci -f -r$rev -d"$date" -w$author $file < $tmpfile >>& $logfile + if ($status != 0) goto ERROR + echo checked into RCS + endif + sccs unedit $file >>& $logfile + if ($status != 0) goto ERROR + end + rm -f $file +end + + +############################################################ +# Clean up +# +echo cleaning up... +mv SCCS old-SCCS +rm -f $tmpfile $emptyfile $initialfile $sedfile +echo =================================================== +echo " Conversion Completed Successfully" +echo "" +echo " SCCS history now in old-SCCS/" +echo =================================================== +set exitval = 0 +goto cleanup + +ERROR: +foreach f (`sccs tell`) + sccs unedit $f +end +echo "" +echo "" +echo Danger\! Danger\! +echo Some command exited with a non-zero exit status. +echo Log file exists in $logfile. +echo "" +echo Incomplete history in ./RCS -- remove it +echo Original unchanged history in ./SCCS +set exitval = 1 + +cleanup: +# leave log file +rm -f $tmpfile $emptyfile $initialfile $sedfile $revfile + +exit $exitval diff --git a/gnu/dist/cvs/doc/.cvsignore b/gnu/dist/cvs/doc/.cvsignore new file mode 100644 index 00000000000..ecbac0e8a4f --- /dev/null +++ b/gnu/dist/cvs/doc/.cvsignore @@ -0,0 +1,34 @@ +*.dvi +Makefile +CVSvn.texi +cvs-paper.ps +cvs.aux +cvs.cp +cvs.cps +cvs.dvi +cvs.fn +cvs.fns +cvs.info +cvs.info-* +cvs.ky +cvs.log +cvs.pg +cvs.ps +cvs.toc +cvs.tp +cvs.vr +cvsclient.info +cvsclient.aux +cvsclient.cp +cvsclient.cps +cvsclient.dvi +cvsclient.fn +cvsclient.info +cvsclient.info-* +cvsclient.ky +cvsclient.log +cvsclient.pg +cvsclient.ps +cvsclient.toc +cvsclient.tp +cvsclient.vr diff --git a/gnu/dist/cvs/doc/ChangeLog b/gnu/dist/cvs/doc/ChangeLog new file mode 100644 index 00000000000..d0faacc739a --- /dev/null +++ b/gnu/dist/cvs/doc/ChangeLog @@ -0,0 +1,3365 @@ +2000-01-11 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo: Add index entries for "Compression" and "Gzip". + Correct typography in many index entries (English phrases should + have initial caps, subcommands/files/etc. should be as-is). + +2000-01-10 Karl Fogel <kfogel@red-bean.com> + + * cvs.texinfo (loginfo): correctly describe CVSROOT/loginfo's + %-expansion behavior. Thanks to Karl Heinz Marbaise + <kama@hippo.fido.de> for noticing the error. + +2000-01-07 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo (Password authentication server): Use -f in example + inetd.conf line. + (Connection): Add advice about using shell script or env to avoid + problems with inetd setting HOME in the server's environment. + (various): Use @file for inetd.conf. + +2000-01-02 John P Cavanaugh <cavanaug@sr.hp.com> + + * cvs.texinfo: document new -C option to update, now that it works + both remotely and locally. + (Re-applied by Karl Fogel <kfogel@red-bean.com>.) + +1999-12-11 Karl Fogel <kfogel@red-bean.com> + + * Revert previous change -- it doesn't work remotely yet. + +1999-12-10 John P Cavanaugh <cavanaug@sr.hp.com> + + * cvs.texinfo: document new -C option to update. + (Applied by Karl Fogel <kfogel@red-bean.com>.) + +1999-11-20 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo(history options): Document -f, -n, and -z. + +1999-11-09 Jim Kingdon <http://developer.redhat.com/> + + * cvsclient.texi (Requests): Document the arguments to "log", now + that I've changed log.c to be more specific in terms of what it + will send. + +1999-11-05 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo: Revert Karl's change once again since the code is now + fixed. Add "Variables" and "User variables" to index. + +1999-11-04 Karl Fogel <kfogel@red-bean.com> + + * log.c (log_usage): Revert Jim Kingdon's reversion of my change + of 1999-11-03. Allowing a space between option and argument + results in lossage; here is a reproduction recipe: run this from + the top of a remote copy of the cvs source tree + + cvs log -d '>1999-03-01' > log-out.with-space + + and then run this (note there's no space after -d now): + + cvs log -d'>1999-03-01' > log-out.no-space + + The resulting files differ; furthermore, a glance at the output of + cvs shows that the first command failed to recurse into + subdirectories. Until this misbehavior can be fixed in the source + code, the documentation should reflect the true state of affairs: + if one simply omits the space, everything works fine. + +1999-11-04 Jim Kingdon <http://developer.redhat.com/> + + * cvs.texinfo (log options): Revert Karl's change regarding -d and + -s. A space is allowed (see sanity.sh for example). + + * cvs.texinfo (Password authentication server): The name of the + file is "passwd" not "password". + + * cvsclient.texi (Top): Add @dircategory and @direntry. + +1999-11-04 Karl Fogel <kfogel@red-bean.com> + + * cvs.texinfo (Password authentication server, Password + authentication client): Rewritten to accommodate the [new] + possibility of empty passwords. + +1999-11-03 Karl Fogel <kfogel@red-bean.com> + + * cvs.texinfo (Invoking CVS): correct documentation for -d and -s + options (as did elsewhere, earlier today). + +1999-11-03 Karl Fogel <kfogel@red-bean.com> + + * cvs.texinfo (Setting a watch): describe `watch off' behavior + more accurately. + +1999-11-03 Karl Fogel <kfogel@red-bean.com> + + * cvs.texinfo (log options): correct documentation for -d and -s + options. There can be no space between these options and their + arguments. + + Also, make sure all @sc{cvs} codes refer to "cvs" in lower case; + this avoids makeinfo warnings. And use @code for the CVSEDITOR + environment variable, not @sc. + +1999-09-24 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo: Misc. formatting cleanups. + +1999-07-16 Tom Tromey <tromey@cygnus.com> + + * cvs.texinfo (admin): Mention admin -k exception. Add cvsadmin + to index. + +1999-07-14 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo (Password authentication server): Note inetd limits + and suggest using shell script to avoid. + +1999-06-01 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Requests): For the import command, the + repository given to the Directory requests is ignored. + +1999-05-27 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Requests): Clarify that Modified, Is-modified, + Notify and Unchanged must specify a file within the current + directory. + +1999-05-24 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (checkoutlist): New node, contains more complete + documentation of this feature. + (CVSROOT storage): Refer to the new node when mentioning + checkoutlist. + (Administrative files): Update the menu entry for Wrappers. + +1999-05-17 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Requests): For Notify request, strike duplicate + "Response expected: no" and fix "a edit" -> "an edit". + +1999-05-14 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Working directory storage): Try to be more clear + about the conflict field. + +1999-05-11 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (config): Use comma after @xref (thanks to Pavel + Roskin for the report/fix). + +1999-05-10 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Requests): Document restrictions on characters + in Notify requests. + +1999-05-04 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Password authentication security): Remove sentence + about how no one has audited pserver for holes; a lot of holes + have been closed, looking for, &c, since that was written. + In the summary, reword to reflect the fact that sniffing a + readonly password does not imply general system access (as far as + I know, of course). + + * cvs.texinfo (Connection): Also suggest inetd -d. + +1999-04-28 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Requests): Say what goes in the "watches" field + of the "Notify" request. + + * cvs.texinfo (Common options): -r is for branches too. + + * cvs.texinfo (Error messages): Add "no such tag" message. + (Common options): -f does not override val-tags check. + +1999-04-26 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Locks): #cvs.rfl locks must start with "#cvs.rfl." + not just "#cvs.rfl". As far as I know CVS has always implemented + the former behavior, and this just fixes the documentation. + +1999-04-23 Yoshiki Hayashi of u-tokyo.ac.jp + + * cvs.texinfo (verifymsg): Correct wrong file name (bugid.edit -> + bugid.verify). + +1999-04-22 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Responses): The text in the "M" response is not + designed for machine parsing. Likewise for "error" in regular + protocol. Likewise for "E" and "error" in authentication protocol. + +1999-04-19 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Error messages): Add "Cannot check out files into + the repository itself". + +1999-04-16 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Other problems): Add the Windows problem with home + directory ending in a slash. + +1999-04-14 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (CVS in repository): Include the format of the + fileattr file here, rather than referring to the CVS source code. + +1999-04-09 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Working directory storage): Whether the timestamp + in CVS/Entries is local or universal actually depends on the system. + +1999-04-05 Derek Price + <http://www-personal.engin.umich.edu/~oberon/resume.html> + + * cvs.texinfo (export options): Remove notation that the -r + tag is sticky. 'cvs export' doesn't store that data. + +1999-04-08 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Error messages): Add "EOF in RCS file" and + "unexpected EOF" (in RCS file) messages. + +1999-03-25 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (admin options): Say there can be no space between + -e and its argument (since the previous sentence said the argument + can be omitted, this is the only possibility). + +1999-02-26 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Merging and keywords): When including conflict + markers, put @asis{} at the start of the line, in case this file + itself is in CVS. Thanks to Derek Price for pointing this out. + +1999-02-25 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo: Refer to "keywords" not "RCS keywords". We had + only used the latter term in a few places, and it seems like a + somewhat odd term in that this style of keyword is by no means + specific to RCS. + (Merging a branch): Remove spurious ")". Use ref, not xref, after + "see". + (Merging a branch, Substitution modes): Make sure that @ref is + followed by comma, since info wants that. + (Merging and keywords): Use samp not code for "-kk". Something of + a judgement call, but the rest of the manual uses samp and that + seems better to me. + (Merging and keywords): Rewrite, to (a) better motivate the + discussion based on what the user wants to do, (b) fix up lots of + convoluted sentences, (c) move the discussion of the binary files + to the end, that is get across the basic idea first and then + embellish it. Remove a few unnecessary index entries. Expand + example. Just tell people to avoid -kk with binary files (comment + out the discussion of using -A after the commit). + +1999-01-29 Derek Price + <http://www-personal.engin.umich.edu/~oberon/resume.html> + + * cvs.texinfo: Added new node/section on merging and keywords. It + contains advice on how to avoid RCS keyword conflicts when merging + and avoid corrupting your binary files while doing it. + +1999-02-24 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Request intro): Add paragraph about transmitting + more than one command. + +1999-01-29 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo: Use EXAMPLE.COM EXAMPLE.ORG and EXAMPLE.NET instead + of domains which might conflict with actual (current or future) + domains. The EXAMPLE domains are registered for this purpose. + +1999-01-22 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Sticky tags): Refer to -j as the better way to undo + a change. + (Merging two revisions): Also talk about undoing removals and + adds. Move the index entries to here. + +1999-01-21 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Error messages): Add "waiting for USER's lock". + +1999-01-16 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Wrappers): Comment out all the -t/-f documentation, + since that feature is currently disabled. + +1999-01-14 Jim Kingdon <http://www.cyclic.com> + + * cvs.texinfo (Connecting via rsh): Add some more index entries so + that people who want to use SSH and such are slightly less lost. + +1999-01-12 Jim Kingdon <http://www.cyclic.com> + + * cvs-paper.ms: Remove comments which contained the FSF's old + address; it has changed. + +1998-12-29 Jim Kingdon <http://www.cyclic.com> + + * cvsclient.texi (Dates): Numeric timezones are preferred. + Also mention the Checkin-time request. + +1998-12-23 Jim Kingdon <http://www.cyclic.com> + + * RCSFILES: Add clarification about certain character set issues + from Paul Eggert, the RCS maintainer. The last paragraph and the + change from Shift-JIS to JIS as an example of a character set + which contains 0x40 bytes which are not '@' characters are mine; + the rest is directly from Paul Eggert. + +1998-12-22 Martin Buchholz <martin@xemacs.org> + + * cvs.texinfo: Fixed various trivial typos. + +1998-12-17 Jim Kingdon + + * cvsclient.texi (Responses): Explicitly say that Mod-time need + not be sent for all files. + +1998-12-16 Jim Kingdon + + Thanks to Ram Rajadhyaksha of the MacCVS Pro team for raising the + following issues. + * cvs.texinfo (Working directory storage): The deal about storing + files as text files applies to all the CVS/* files, not just + CVS/Entries. State the rationale too. + Document CVSROOT/Emptydir in CVS/Repository. + There is no set order in CVS/Entries. + Explicitly say that writing Entries.Log is optional. + +1998-12-03 Jim Kingdon + + * cvs.texinfo (Error messages): Add "unrecognized auth response". + (Password authentication server): Remove comment about + "unrecognized auth response" and link to the troubleshooting + section. + +1998-12-02 Jim Kingdon + + * cvs.texinfo (Multiple repositories): Add an example. + +1998-11-18 Jim Kingdon + + * cvs.texinfo (Invoking CVS): Change "-r tag" to "-r rev". We + already use "tag" as the name of the tag we are adding. + +1998-11-13 Jim Kingdon + + * cvs.texinfo (CVS commands): Add comment about whether part of + the manual should be organized by command. + +1998-11-06 Jim Kingdon + + Clean up various confusions between modules and directories: + * cvs.texinfo: In "are you sure you want to release" message, + change module to directory. CVS was changed some time ago. + (Tags): "working copy of the module" -> "working directory". + (Merging two revisions): Remove unnecessary text "that make up a + module". + (Recursive behavior): Change "module" to "directory". + (Removing files): Likewise. + (Tracking sources): Remove "a module" from titles. + (Moving directories): Change "module" to "parent-dir". + (Inside): Remove "of the module". + (Inside): Change "module" to "dir". + (Rename by copying): Change "module" to "dir". + (Rename by copying): Remove "of the module". + (Moving directories): "copy of the module" -> "checked out copy of + the directory"; remove second "of the module". Change "check out + the module" to " check out again". + (Moving directories): Remove "of the module". + (Keyword substitution): "your working copy of a module" -> "a + working directory". + (CVS commands): Change "module" to "directory". + (release examples): "module" -> "tc directory". + (commitinfo): "relative path to the module" -> "directory in the + repository". + (verifymsg): Change "module" to "directory". + (Updating a file): "working copy of a module" -> "working directory". + +1998-10-25 Jim Kingdon + + * cvs.texinfo (Branches and revisions): Fix error in branch + numbering which was introduced with change of 4 May 1997. + +1998-10-20 Jim Kingdon + + * cvs.texinfo (Tags): Point to Invoking CVS node so people aren't + left wondering what the syntax is. When introducing -r option, + warn people about sticky tags right off. + (Tagging the working directory, Tagging by date/tag, Modifying + tags, Tagging add/remove): New sections. + (Invoking CVS): Adjust tag and rtag to point to the new sections, + and to add tag -c which had been omitted. Delete tag -n; there is + no such option. + (rtag, tag): Removed; no longer needed. + (commit examples): Update xref. + +1998-10-15 Jim Kingdon + + * cvsclient.texi (Requests): It is OK to send Set before Root. + +1998-10-13 Jim Kingdon + + * cvsclient.texi (Protocol Notes): Remove item about "cvs update" + sending modified files to the server; there are some better ideas + at http://www.cyclic.com/cvs/dev-update.txt + Add mention of www.cyclic.com. + +1998-09-30 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Committing your changes, Environment variables): + Document VISUAL. + +1998-09-27 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication server): Say explicitly + that you edit passwd directly, many users get confused by this. + +1998-09-24 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Connecting via fork): :fork: may be of interest to + users, for example those who prefer CVS to prompt for one log + message per checkin, rather than one per directory. + (Connecting via fork): Document CVS_SERVER. + +1998-09-24 Noel Cragg <noel@swish.red-bean.com> + + * cvs.texinfo (Connecting via fork): new node about the fork + access method. + +1998-09-22 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Environment variables): Document + CVS_IGNORE_REMOTE_ROOT in the CVS 1.10 context. + (Moving a repository): Update comments concerning surgery on + CVS/Root and CVS/Repository files. + +1998-09-21 Noel Cragg <noel@swish.red-bean.com> + + * cvs.texinfo (Environment variables): remove information about + CVS_IGNORE_REMOTE_ROOT, since it's no longer used. + +1998-09-21 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (config): Mention that CVS 1.10 doesn't have + LockDir. + +1998-09-18 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list): Describe $Name and checking out with + a revision. + +1998-09-16 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: RFC2346 is out; update comment. + +1998-09-13 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list, Substitution modes): In describing + $Locker and -kkvl, refer to cvs admin -l. + + * cvsclient.texi (Requests): Re-word description of Sticky to + allow room for "Ntagname" (or other, future, values). + + * cvs.texinfo (tag): Remove confusing wording about supplying + revision numbers "implicitly". + +1998-09-10 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (rdiff options): Thanks to the diff library, -u is + supported regardless of your diff program. + +1998-09-07 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (config): Add LockDir. + +1998-09-01 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): "Directory" and "Argument" are + requests, not commands. Likewise for "other-request". A command, + roughly, is a request that uses "Argument"s, but we might want to + phase out the use of that term more so than codify it, I'm not sure. + +1998-09-01 Noel Cragg <noel@swish.red-bean.com> + + * cvsclient.texi (Requests): added a detailed explanation of the + Directory request and how it is handled, both for pre-1.10 and + post-1.10 servers. + +1998-09-01 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Multiple repositories): Also describe the CVS 1.10 + behavior. Looking at a mismatched version of the manual seems to + be a reasonably common occurrence. + + * cvs.texinfo (Environment variables): Revert change regarding + CVS_SERVER_SLEEP*; having that kind of debugging code in the main + CVS is getting out of hand. + +1998-09-01 Noel Cragg <noel@swish.red-bean.com> + + * cvs.texinfo (Multiple repositories): brief mention that cvs now + handles a working directory composed of multiple repositories. + (Environment variables): add note about CVS_SERVER_SLEEP2. + +1998-08-21 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Text tags): Document importmergecmd tag. + +1998-08-20 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Replace out of date URL concerning + ISO8601 dates with a more general statement and a few comments. + +1998-08-18 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Add "Checkin-time" request. + +Sun Jul 26 02:42:20 1998 Noel Cragg <noel@swish.red-bean.com> + + * cvs.texinfo (config): TopLevelAdmin variable. + + * cvsclient.texi (Requests): fix typo. + +1998-07-14 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): "remove" is like "add" in the sense + that it is the "ci" request which does most of the work. + +1998-06-23 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Excluding directories): Fix order of + "!first-dir/sdir" and "first-dir" to match what CVS actually + accepts. Reported by Tim McIntosh of sterling.com. + +1998-06-09 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Using keywords): Rewrite to be less specific to + source code in C. The old text was worse than that; it was + specific to certain versions of GCC (not even current GCC's, I + don't think) (reported most recently by Mitchell Perilstein; + if memory serves by others before that). + +1998-06-08 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Concurrency): Also mention #cvs.lock. Don't + mention #cvs.tfl; it is quite old (before CVS 1.5). + (Locks, Backing up, Concurrency): Add more index entries. + +1998-06-03 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Tracking sources): Clarify that the vendor branch + is only made the head revision when you import a new file, not any + time you import a file. + +1998-05-23 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (What is CVS?): info-cvs-request is now at gnu.org + and is no longer handled by a human (hallelujah). + +1998-05-12 Jim Meyering <meyering@ascend.com> + + * cvs.texinfo: Add an info dir entry. + Remove trailing white space. + +1998-05-05 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Be more explicit that -m 'COPY' has no + effect on binary files. + +1998-05-02 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: Add more discussion of the order of the revisions. + +1998-04-27 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (loginfo example): Also give example of sending + mail. Use internal variable $USER rather than expecting CVS to + set the environment variable $USER. Change unnecessary 'sed' + invocation to 'cat' (it suffered from the same problem in terms of + internal variables versus environment variables). + + * cvs.texinfo (Error messages): Add "conflict: removed FILE was + modified by second party". + +1998-04-20 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Update comment about meaning of + HEAD in cvs diff. + +1998-04-12 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Dates): Also mention log -d. + + * cvs.texinfo (Invoking CVS): No space is allowed between -r or -w + and its argument, for the log command. + +1998-04-11 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Dates): New section, explaining the deal with + date formats. + +1998-04-09 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Global options, Invoking CVS): Fix typo + ("files files" -> "files"). + (Invoking CVS): Make -q and -Q more concise. + (Invoking CVS): Use @var for metavariables in "diff -r". + +1998-03-17 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (~/.cvsrc): In example, put "checkout" rather than + "co" into .cvsrc; we just finished explaining that only the former + works! Thanks to Lenny Foner for reporting this. + + * cvs.texinfo (Copying): Remove this node. This basically + restores the status quo prior to 18 Oct 1996 (before then the node + existed but was empty). + (before Top): Adjust copyright notice accordingly. + +1998-03-12 Tim Pierce <twp@skepsis.com> + + * RCSFILES: Updated description of `hardlinks' newphrases. + +1998-03-07 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tags, Sticky tags, Creating a branch, Accessing + branches): Rename release-0-1 tag to rel-0-1 and likewise for + release-0-1-patches and release-0-4. This fixes an overfull hbox. + (diff options): Reformat table to fix underfull hboxes and such. + +1998-03-07 Tim Pierce <twp@skepsis.com> + + * cvs.texinfo (Editing files, Special Files): Document hardlinks. + Various cleanups to PreservePermissions text. + * RCSFILES: Document PreservePermissions newphrases. + +1998-03-04 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Special Files): Add notes about client/server CVS + and hard links across directories. + +1998-03-01 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keeping a checked out copy): The magic loginfo + incantation isn't too likely to work except on unix. + +1998-02-23 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (user-defined logging): Double "@" literal. + +1998-02-18 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (user-defined logging): Add taginfo example. + +1998-02-04 Tim Pierce <twp@skepsis.com> + + * cvs.texinfo (config): PreservePermissions variable. + (Special Files): New. + (Editing files): Add note about PreservePermissions. + +Tue Feb 10 18:07:35 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Connection): New node. + + * cvsclient.texi (Protocol): Fix typo (lots -> lost). + +Sun Feb 8 21:39:22 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Invoking CVS): For admin -b, point to the section + where we talk about reverting to vendor branch. + + * cvs.texinfo (Invoking CVS, rdiff options): Document rdiff -V + option as obsolete, since it was made a fatal error some time ago. + + * cvs.texinfo (Invoking CVS): Add global options, keywords, and + keyword substitution modes. Wording fix in reference to --help + and Index. + +Wed Jan 28 23:09:39 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Excluding directories): Add index entry for "!". + +28 Jan 1998 Karl Fogel and Jim Kingdon + + * cvsclient.texi (Requests, Responses): document + "wrapper-sendme-rcsOptions" and "Wrapper-rcsOption". + +Tue Jan 27 18:37:37 1998 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Excluding directories): New node, documenting how + to exclude directories using ! in an alias module. + +Sun Jan 18 18:23:02 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Add Kopt request. + +Thu Jan 1 17:36:42 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (BUGS, Credits): Change @unnumbered to @appendix now + that these are moved from the start to the end. + +Sat Dec 27 10:06:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "Too many arguments!". + +Fri Dec 26 18:30:26 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (What is CVS?): Just point to the two canonical web + sites (Pascal Molli and Cyclic) concerning CVS downloads. The GNU + URL was out of date and GNU only has source distributions anyway. + + * cvs.texinfo: Change bug-cvs address to gnu.org per email from + Martin Hamilton. + +Tue Dec 23 18:04:09 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Sticky tags): Further cleanups. Fix thinko + (s/subsequent cvs update/& commands/). Remove "vi driver.c" and + commit from example (totally vestigial). Reword start of + paragraph on non-branch sticky tags, so that it better alludes + to branch sticky tags. When introducing sticky tags, make it + clear that even people who aren't trying to use sticky tags + may need to know how to avoid them. Restore comment about + CVS/Tag files. + (Accessing branches): Don't xref to merging here; that is a much + more advanced topic and the "but see" wording didn't tell us what + to see the xref about. + +Tue Dec 23 14:39:08 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon + + * cvs.texinfo (Creating a branch): Rewritten. Introduce with + `tag', then discuss `rtag' and `-r'. + +Tue Dec 23 10:03:37 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon + + * cvs.texinfo: Changes to dehairify the "Sticky tags" situation: + (Revisions): "Sticky tags" moved here, description in menu changed + to be a little more informative. + (Sticky tags): Moved from "Branching and merging" to "Revisions". + (Accessing branches): New node in "Branching and merging", + explains how to use checkout vs update to retrieve a branch. + Text and example inherited from "Sticky tags", but text mostly + rewritten. + (Sticky tags): Moved under "Revisions", rewritten somewhat (more + rewrites to follow). + Don't use "-v" in "cvs status" example. + +Mon Dec 22 11:46:05 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon + + Cleanups related to recent separation of revisions from + branching/merging: + * cvs.texinfo (Revisions): Take paragraph introducing branches, + rewrite it and move it to "Branching and merging". + (Branching and merging): Also rewrite merging intro. + (Revision numbers): Don't go into detail about branch revision + numbers here, just mention that they happen and refer to new + node "Branches and revisions". + (Branches and revisions): New node under "Branching and merging", + inherits text from "Revision numbers". + (Creating a branch): Refer to "Branches and revisions" now, not + "Revision numbers". + (Binary why): Rewrite sentence which refers to merging, so that + it isn't specific to branch merging. + (Branches motivation): Fix typo (select -> elect). Add comment + about what this node is accomplishing, in general. + +Sun Dec 21 20:57:24 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon + + This is just moving text; related cleanups to follow. + * cvs.texinfo: Changes to put branching and merging together, and + keep it all separate from revisions: + (Revisions): Renamed from "Revisions and branches". + (Branching and merging): Renamed from "Merging". + (Branches motivation, Creating a branch, Sticky tags, Magic branch + numbers): these subnodes moved to "Branching and merging" from + "Revisions". + everywhere: Adjusted cross-references to cope with above. + +Sun Dec 21 20:36:39 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon + + Note that this is just moving text, not changing it: + * cvs.texinfo: divide top-level menu into sections. + (Multiple developers, Builds, Tracking sources, Keyword + substitution): moved to be in "CVS and the Real World" section. + (Compatibility): moved to be in "References" section. + +Mon Dec 22 08:54:31 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Example): In comment, in citing the BNF style + used in many RFCs, cite RFC2234 not RFC822 (now that the former is + out). + +Sun Dec 21 17:42:22 1997 Karl Fogel <kfogel@floss.red-bean.com> + + * cvs.texinfo (Overview): New node. + (What is CVS?, A sample session): Put under Overview. + (What is CVS not?): New node under Overview. + [text previously was part of "What is CVS?" -kingdon] + (Preface): Removed this node and its contents. + (Checklist): Removed this node and its contents. + (Credits): Now toward end of top-level menu (was under Preface). + (BUGS): Now toward end of top-level menu (was under Preface). + +Sun Dec 14 10:14:25 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add MT response. + (Text tags): New node. + + * cvs.texinfo (loginfo): Add comment about which commands run + loginfo. + +Sat Dec 13 08:41:13 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): State that + GSSAPI is preferred to kserver. Try to be clearer about what + the term "pserver" means. Introduce GSSAPI and cite the relevant + RFCs. Discuss the limitations of the existing features in + preventing hijacking. + + * cvs.texinfo (GSSAPI authenticated, Kerberos authenticated): + Briefly introduce what GSSAPI and Kerberos are. Be slightly more + emphatic about protecting against downgrade attacks. + +Fri Dec 12 17:36:46 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (GSSAPI authenticated): New node. + (Global options): Document -a. Mention GSSAPI in -x + documentation. + * cvsclient.texi (Connection and Authentication): Document GSSAPI + authentication. + (Requests): Add Gssapi-encrypt and Gssapi-authenticate. + +Fri Dec 12 09:27:38 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (cvsignore): Add note about comments and the + space-separated nature of the syntax. + +Sun Dec 7 09:33:11 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (checkout): Clarify issues regarding updating + existing working directories. + +Sun Nov 30 20:38:17 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Add comment: we don't document %s. + +Mon Nov 24 23:00:09 1997 Karl Fogel <kfogel@floss.red-bean.com> + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi: Move Protocol Notes node to the end. + + * cvsclient.texi (Request intro): new node/section. + (Protocol): added some introductory material. + Rearranged menu into General Conventions, Protocol specification, + and Example etc sections. + (File Modes): replaces Modes, for consistency. + +Sat Nov 22 12:29:58 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Entries Lines): Clarify options in entries line. + +Tue Nov 18 09:23:15 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Be more explicit about "export" and + entries lines. + + * Makefile.in (DISTFILES): Remove DIFFUTILS-2.7-BUG. + +Mon Nov 17 18:20:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (tag options): Expand comment with reference to FAQ. + +Fri Nov 14 11:02:37 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Update discussion of "dying gasps". + + * cvs.texinfo (tag options): Add FIXME comment about renaming tags. + +Thu Nov 13 10:20:39 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Remove also has a -f option with a + different meaning than most. + +Wed Nov 12 21:57:40 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File permissions, Connecting via rsh, Environment + variables): When putting an environment variable in the index, say + it is an environment variable. Don't index the same name twice. + + * cvs.texinfo: Many edits to reflect the fact that CVS no longer + invokes external RCS programs. + +Tue Nov 11 15:15:49 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Locks, CVS in repository): New nodes, document the + locking scheme and briefly outline CVS and CVS/fileattr. + +Sun Nov 9 17:39:41 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * DIFFUTILS-2.7-BUG: Removed; the bug is fixed and the testcases + are incorporated into sanity.sh. + +Sat Nov 8 09:49:38 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Binary why): Try to be a little clearer about how + merges fit into CVS. Say it may be error prone to have developers + doing merges manually. + +Tue Nov 4 13:02:22 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin options): Add discussion of what happens if + there are tags. + +Fri Oct 31 00:04:09 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin options): Rewrite discussion of -o to + hopefully be clearer and to also document the new :: syntax. + (admin examples): Removed; incorporated into admin options. + (Invoking CVS): Wording fix for admin -o. + + * cvs.texinfo (Binary why): New node, talks about diff and merge. + (Binary howto): Renamed from Binary files. + (Binary files): Now just contains an introduction. + + * cvs.texinfo (Error messages): Add "could not merge" message. In + discussion of "Binary files . . . differ" message, mention that + this is only an issue with old verisons of CVS. + +Thu Oct 30 15:55:21 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "authorization failed" message. + +Wed Oct 29 11:52:05 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Remove fake RCSid; we decided to remove rcsid's a + while ago. Cleanups suggested by Stephen Gildea (CVSROOT/passwd + has 2 or 3 fields; /user -> /usr; noone -> no one; in used -> in + use). Add comment about making compilers happy about rcsids. + +Sat Oct 25 00:58:24 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: rcsfile.5 is correct about {num} after next being + optional. + +Wed Oct 22 10:08:27 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add message about unrecognized + response from cvs server. + +1997-10-11 Noel Cragg <noel@swish.red-bean.com> + + * cvs.texinfo (checkout options): describe how the `-d' and `-N' + flags really work. Give examples. + (export options): refer the reader to the descriptions for `-d' + and `-N' in checkout options, since the behavior is the same. + +Thu Oct 9 12:01:35 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (log options): Add comment about "cvs log -r". + +Wed Oct 8 10:24:19 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (rtag options): Add comment about how this is + confusing. + +Tue Sep 30 12:31:25 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): Add comment about + Entries.Static. + +Thu Sep 25 23:52:57 1997 Noel Cragg <noel@swish.red-bean.com> + + * cvsclient.texi (Responses): description of Module-expansion was + missing a carriage return after the @item clause. + +Wed Sep 24 12:04:42 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Remote repositories): Add comment about pserver + vs. having users create their own repositories. + +Sat Sep 20 00:59:53 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list): Change title from "RCS Keywords" to + "Keyword list" as it is CVS that expands them. + (Avoiding substitution): Change "rcs" to "cvs", in the context of + the program which expands keywords. + +Fri Sep 19 22:57:24 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: Grammar fix in first paragraph. Re-word section on + dead newphrase. Add item about what it means if "expand" is omitted. + + * cvs.texinfo (Magic branch numbers): Change example branch number + from 1.2.3 to 1.2.4; CVS assigns even branch numbers and I don't + think vendor branches are very relevant to this example. + +Wed Sep 17 17:21:33 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin options): Add comment about "cvs admin -b" + (with no argument to the -b). + + * RCSFILES: "next" is optional, not required. + +Tue Sep 16 15:13:22 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Binary files): Add comment about another possible + way to auto-detect binary files. + +Sun Sep 14 12:38:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Conflicts example): Adjust text and comments + regarding conflict markers to reflect change in CVS. + +Wed Sep 10 12:44:04 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Server requirements): Add comment about server + disk usage in /tmp. + + * cvs.texinfo (Common options): More comments about date formats: + "now", "yesterday", and the "3 weeks ago" family. + +Tue Sep 9 13:09:58 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * DIFFUTILS-2.7-BUG: Eggert patch is preferred to Rittle one. + +Sun Sep 7 18:38:23 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (history options): Revise -e to say that it includes + future record types (and remove out of date list of what record + types it implies). + + * cvs.texinfo (Environment variables): Expand/correct discussion + of HOME, HOMEDRIVE, and HOMEPATH. + (Error messages): Add "could not find out home directory". + + * cvs.texinfo (update options): Reword -r doc to hopefully be + clearer that it takes either numeric or symbolic revision. + + * cvs.texinfo (syntax): Add comment about how regexp syntax may + be, er, creatively altered, by configure.in. + +Sat Sep 6 11:29:15 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): Document Baserev and + Baserev.tmp. + (Working directory storage): Adjust comment regarding CVS/* being + text files. + +Fri Sep 5 14:42:39 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (BUGS): Remove mention of unsupported resources page + on http://www.cyclic.com, as it might go away in a future + reorganization. + + * DIFFUTILS-2.7-BUG: Further info from Eggert. + +1997-09-05 Paul Eggert <eggert@twinsun.com> + + * DIFFUTILS-2.7-BUG: Explain how this bug will probably be + fixed in the next diffutils release. + +Thu Sep 4 17:09:57 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Binary files): Reword the section on what you need + to do with cvs admin -kb to hopefully be a bit clearer. Still not + ideal (see comment). + + * cvs.texinfo (modules): Break node into separate nodes for alias + modules, regular modules, ampersand modules, and options. Expand + text with more examples and explanations. Add index entries. + +Wed Sep 3 14:49:43 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Multiple developers): Add idea about cvs editors + and reserved checkouts. + +Sun Aug 31 19:36:21 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Rewrite paragraph on cvs add on a + filename containing '/'. + +Thu Aug 28 14:13:50 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (diff options): Add comment about "cvs diff" + vs. "cvs diff -r HEAD". + + * cvs.texinfo (Global options): Add comment about -w not + overriding cvs watch on. + +Wed Aug 27 08:09:31 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication server): Grammar fix ("under + as the username" -> "as the username"). + + * cvs.texinfo: Fix doubled 'the the' typos. Reported by + karlb@atg.com. + +Tue Aug 26 12:25:42 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Checklist): Reword xref to point to Binary files + rather than Keyword expansion. Credit goes to jeff@alum.mit.edu + (Jeff Breidenbach) for reporting the problem. + +Mon Aug 18 17:23:18 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (modules): Suggest taginfo instead of -t. Add + comment with some of the reasons. Add comment about -u and -i + problems. + +Sat Aug 16 10:19:06 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add note about how "could not + check out foo.c" seems to also have been observed on Irix. + +Fri Aug 15 17:28:01 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "could not check out foo.c". + +Thu Aug 14 23:57:53 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Document new -m 'COPY' behavior. + +Tue Aug 12 20:56:40 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Sticky tags): Add comment about how we should be + documenting sticky tags. + +Fri Aug 8 10:01:03 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File status): Add comment about "working revision" + in cvs status for a locally removed file. + +Thu Aug 7 22:53:45 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (From other version control systems): Mention + pvcs_to_rcs alongside sccs2rcs. + +Tue Aug 5 17:22:50 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Compatibility): Add comment about how CVS probably + could be detecting the case of dead files killed by CVS 1.3. + + * cvs.texinfo (From other version control systems): Add paragraph + about converting from systems which don't export RCS files. + +Sun Aug 3 21:03:14 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Cite RFC1321 for MD5. + + * cvs.texinfo (A sample session): Nuke index entry for "A sample + session". The fact that this isn't "sample session" is totally + bogus, but in general the table of contents is probably better for + this entry. + + * cvs.texinfo (Error messages): Add comment about wording of error + concerning unknown -x option. + + * cvs.texinfo (Wrappers): Add comment about absolute filter pathname. + +Thu Jul 31 14:40:15 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Use @ref not @xref when reference is not at the + start of a sentence. Avoids capitalizing "See" when we shouldn't. + Fixes to other similar xref problems. + +Wed Jul 30 19:30:31 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): Don't use @samp + on BEGIN AUTH REQUEST and friends. Avoids overfull hbox. + +Fri Jul 25 10:40:22 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Remove obsolete sentence regarding + using Directory instead of Repository enabling alternate response + syntax. + + * cvsclient.texi (Response intro): Add discussing of file updating + responses and file update modifying responses. + (Responses): Refer to this description rather than trying to + describe it in each place. The descriptions in each place were + somewhat incomplete and didn't get updated when new file updating + responses were added. + + * cvsclient.texi: Split node Responses into Response intro, + Response pathnames, and Responses. + +Thu Jul 24 23:13:24 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (config): Document SystemAuth. + (Password authentication server): Mention SystemAuth. + +Mon Jul 21 08:57:04 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (DISTFILES): Add DIFFUTILS-2.7-BUG. + +Sun Jul 20 17:55:52 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin options): For options with optional + arguments, specify that there can be no space between the option + and its argument. For -N, add xref to Magic branch numbers. For + -t, talk about reading from stdin. Comment changes. + +Sat Jul 19 22:28:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Preface): Make section titles more verbose. + Likewise for the menu. + +Fri Jul 18 08:41:11 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): No need for an external patch if + server and client are current. Add comment with more thoughts + about messages specific to old versions of CVS. + + * cvs.texinfo (Error messages): Add "cannot start server via rcmd". + + * cvs.texinfo (Error messages): Add "cannot open CVS/Root" for cvs + init. + + * cvs.texinfo (Error messages): Add "missing author". + +Tue Jul 15 16:47:08 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list): Fix documentation of $Log to reflect + the fact that we no longer use the comment leader. + (admin options): Fix documentation of $Log. + (admin examples): Remove example concerning comment leader, since + the example no longer does what it claims to. + (admin, admin options): Fix various parts of the documentation to + not refer to this being implemented via RCS. Say nastier things + about -I and -x. Add comments about options to "rcs" which we + don't document. + +Mon Jul 14 00:04:32 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): The "cannot change permissions on + temporary directory" error has been happening in various test cases. + +Sat Jul 12 11:12:18 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Repository files): Further comments about leading + "-" in filenames. + +Fri Jul 11 21:30:11 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Repository files): Add comment about legal + filenames. + +Wed Jul 9 18:05:26 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add Mbinary response. + +Mon Jul 7 12:04:01 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Goals): Add previously unwritten goal about only + one way to do each operation. + + * cvs.texinfo (File permissions): Rewrite paragraph on setuid to + be more verbose and less unix-specific. + +Sat Jul 5 03:16:38 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): When we said to + "ignore" an unrecogized code we mean to treat it as nonspecific, + not to ignore the response. + + * cvsclient.texi (Example): Refer to RFC2119 when referring to + terminology of MUST, SHALL, &c. + + * cvs.texinfo (Windows permissions): New node. + +Fri Jul 4 15:27:43 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Common options): Fix typo (avaliable for + available). + +Tue Jul 1 09:19:02 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Server requirements): Discuss memory used by diff. + + * cvs.texinfo (Substitution modes): Add comment about -A resetting + both sticky tags/dates and sticky options. + + * cvs.texinfo (File permissions): Add paragraph concerning + ownership of the RCS files. + + * cvs.texinfo (Working directory storage): Relative repositories + in CVS/Repository are legal. + +Mon Jun 30 10:48:21 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Top): Add menu item for Password scrambling. + + * cvs.texinfo (Committing your changes): Add comment concerning + documentation of message prompting. + +Fri Jun 27 11:20:34 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Password scrambling): New node. + (Connection and Authentication): Adjust accordingly. + (Protocol Notes): Add long discussion of character sets and + password scrambling. + + * cvs.texinfo (Repository files): Also mention doc/RCSFILES in + documenting RCS file format. + (CVSROOT, storage of files): New node. + +Thu Jun 26 09:18:15 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File permissions): xref to the pserver thing about + permissions in CVSROOT. + (Kerberos authenticated): Explicitly mention kerberos rsh. + Add various index entries for "security, <foo>". + +Wed Jun 25 13:39:16 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Rewrite comments concerning HEAD + and testcases and solution. Changing HEAD might be too big a + change; might be better to phase it out. + (Common options, Tags): Add index entries for HEAD and BASE. + +Tue Jun 24 09:37:26 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add assertion failed. + + * cvsclient.texi (Connection and Authentication): Add "E" and + "error" as responses in authentication protocol. The server + already was in the (formerly bad) habit of sending them, and we + might as well implement this in the client and document it. + + * cvs.texinfo (Password authentication security): Note about + permissions on $CVSROOT also applies to its parent and so on up to + /. + +Mon Jun 23 18:28:18 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Creating a repository): xref to Server requirements + for more details on memory, CPU. + (Server requirements): Add xref to Creating a repository regarding + disk space. + + * cvs.texinfo (Read-only access, Password authentication + security): The known holes which let a read-only user execute + arbitrary programs on the server are gone. + + * cvsclient.texi (Protocol Notes): Remove multisite item; it is + replaced by item 186 in TODO. Add a general reference to TODO. + Rewrite accordingly the sentence about multisite in the item + concerning sending modified files in "cvs update". + +Fri Jun 20 17:00:20 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "binary files differ" when + trying to check in a binary file. + +Fri Jun 20 14:01:23 1997 David J MacKenzie <djm@va.pubnix.com> + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Fix various formatting, spelling, stylistic, and + factual errors. + +Thu Jun 19 07:11:33 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (config): New node. + (Password authentication server): Talk about RCSBIN in config as + an alternative to -b global option. + * cvsclient.texi (Requests): Specify when Root can/must be used. + + * cvs.texinfo (Error messages): Add + "*PANIC* administration files missing". + + * cvs.texinfo (Password authentication server): Mention + permissions on $CVSROOT and $CVSROOT/CVSROOT as part of the + installation process. + (Password authentication security): Clarify that permissions issue + applies to $CVSROOT as well as $CVSROOT/CVSROOT. + +Wed Jun 18 00:03:25 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication security): Add paragraph + on write permissions of $CVSROOT/CVSROOT. + + * cvs.texinfo (Adding and removing): New node. Move Adding files, + Removing files, Removing directories, Moving files, and Moving + directories under it. + + * cvs.texinfo (Removing directories): Add sentence about how + one doesn't remove the directory itself. + + * cvs.texinfo (Password authentication server): Document + --allow-root. + +Tue Jun 17 09:58:03 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "unknown option" from RCS. + +Fri Jun 13 12:11:09 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Global options): Add note about how -n might affect + CVS's output. + +Thu Jun 12 09:33:40 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Other problems): New node. Add discussion of + problem with old rcsmerge. + + * cvs.texinfo (Environment variables): Add CVSUMASK. + +Mon Jun 2 18:39:57 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Moving a repository): New node. + +Tue May 27 18:27:57 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): Add comment about + timestamps. + * cvsclient.texi (Responses): Add Mod-time. + +Mon May 26 10:04:32 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Add comment concerning -t/-f and + client/server. + +Sun May 25 00:08:39 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Multiple vendor branches): New node. + (First import, import options, Invoking CVS): xref to it. + +Sat May 24 23:47:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File permissions): Add comment about group + ownership in repository and setgid bit on directories. + +Fri May 23 17:14:05 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: Fix typo in dead newphrase description ("an" -> "a"). + +Fri May 23 16:33:38 1997 Ian Lance Taylor <ian@cygnus.com> + + * RCSFILES: Mention dead as a newphrase. + +Fri May 23 09:45:39 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Builds): In comment, update URL of mk. + +Thu May 22 09:25:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add comment about yet another way + to produce a "cannot open CVS/Entries for reading" error. + +Tue May 20 17:54:55 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add item about EINVAL in rename. + +Mon May 19 00:21:49 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keywords in imports): New node. + (Tracking sources): Add comment about what a "vendor" is. + + * cvs.texinfo (Keyword substitution): Where it refers to RCS + having a certain behavior, rewrite to not pass the buck like + that. Saying "RCS file" is still OK; that is a legit CVS + concept. A few other minor edits. + +Sun May 18 10:24:57 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: Add list of known newphrase extensions. + + * cvs.texinfo (From other version control systems): Fix typo + ("systesm" -> "systems"). + + * cvs.texinfo (Exit status): New node. + (diff): Replace text on exit status with an xref to that node. + The previous text documented a behavior which CVS no longer + implements. + (user-defined logging, commitinfo, verifymsg, Error messages): + Add index entries for "exit status, of <something which CVS invokes>". + + * cvs.texinfo (Administrative files): Add comment concerning + writing triggers and particularly performance issues. + + * cvs.texinfo (rtag options, tag options): Don't discuss what old + versions did with respect to the behavior now controlled by -F; we + don't try to document old versions here. Add comments concerning + how -F should be documented. Add index entries for "renaming + tags" and such pointing to "tag -F". + +Wed May 14 12:16:19 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Binary files): Add text and comment about + automatically detecting binary files. + +Mon May 12 11:55:07 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): Add item about + future expansion. + +Thu May 8 11:08:34 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Update imports): Add comment about wdiff + vs. fsf/wdiff in example. + +Wed May 7 13:52:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (checkout): Add comment about need for example + regarding what the "module" argument means. + +Tue May 6 18:02:27 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (History browsing): Add comment about looking at old + revisions. + +Tue May 06 15:05:00 1997 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo: More additions/corrections for -R due to recent + changes. + +Mon Dec 16 15:18:00 1996 Larry Jones <larry.jones@sdrc.com> + + * cvs.texinfo: Added/corrected documentation for -R. (Minor edits + by Jim Kingdon to reflect recent changes in cvs.texinfo) + +Sun May 4 14:38:35 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Compatibility): Add comment about "D" lines in + Entries. + + * cvs.texinfo (CVS commands, diff): Change "run diffs" to "show + differences"; the former is jargon. + (CVS commands): Don't refer to "rlog" in describing what log does. + + * Makefile.in (cvsclient.dvi cvsclient.aux): Run texi2dvi rather + than (poorly) emulating it ourself. + + Fix overfull and underfull hboxes: + * cvs.texinfo (What is CVS?): Add words "the newsgroup" before + "comp.sources.unix". + (Credits): Put list of people in @display. + (Repository files): Put /usr/local/cvsroot in @example. + (Connecting via rsh): Change "anklet" to "toe" in example. + (Kerberos authenticated, Password authentication client, Password + authentication server): Change "brickyard" to "yard" in example. + (Read-only access): Use @example and refer to files with a shorter + pathname. + (Server temporary directory): Use @example for pathname. + (Watches Compatibility): Add phony line break. + (Revision numbers): Remove revision 1.2.2.2 and tighten up the + spacing for "the main trunk". + (Tags, Creating a branch): Change /usr/local/cvsroot to /u/cvsroot. + (Merging more than once): Tighten up spacing for "the main trunk". + (Recursive behavior): Put long command in @example. + (First import): Remove word "called". + (Common options): Put long URL in @example. + (loginfo example): Use fewer hyphens in example. + (Variables): Put long command name in @example. + (Copying): Add line break. + (Administrative files): Remove "the" from title. + (Copying): Change "@unnumberedsec" to two "@heading"s. + * cvsclient.texi (Requests): Change /home/kingdon/zwork/cvsroot to + /u/cvsroot. + (Example): Add word "file". + (Example): Change line breaks in example log message. + (Example): Change /home/kingdon/testing/cvsroot to /u/cvsroot. + + * cvs.texinfo (Credits): Don't refer to appendix A and B, they + have been renumbered. Reword so that it works whether the text in + question has since been rewritten or not. + + * cvs.texinfo (BUGS): Rewrite to reflect the many different ways + that one might want to handle bugs. Move information on Signum + and Cyclic from Preface to here. Remove information on known + deficiencies in the manual (some of them I'm not sure were really + things in need of improvement; others were too general to be + useful). For the most part FIXME comments are probably better for + this. Remove "Linkoping, October 1993, Per Cederqvist"--many + parts of the manual are now from other people, dates, and places. + (What is CVS): For the most part, just refer to BUGS concerning + bug-cvs. Also tell people how to subscribe to bug-cvs. + (Credits): Say that list is not comprehensive and refer to + ChangeLog. + +Sat May 3 10:51:58 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (rcsinfo): Add comment about checkoutlist and + related topics. + + * cvs.texinfo (Server temporary directory): New node. + + * cvs.texinfo (Backing up): New node. + + * cvs.texinfo (Repository): Be more explicit about the repository + and the working directory not being subdirectories of each other. + +Mon Apr 28 11:12:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Removing files): Use "*.c" not "?.c" in example; + the former should be good for both unix and DOS-like operating + systems. Document -f option. Refer to Invoking CVS for a full + list of options. Add a few comments. + + * cvs.texinfo (Invoking CVS): For checkout and update, call them + "sticky options" not "sticky kopts". + + * cvs.texinfo (Editing files): Add additional comments on get + vs. checkout. + +Sun Apr 27 16:17:06 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (commit): Only document the current flags (where -f + is force and -F file gets the message from a log file). We had + partly made this change on 9 Feb 1997, but some places got missed. + + * RCSFILES: Add discussion of the common concern regarding + applying deltas to get to a branch head. + + * DIFFUTILS-2.7-BUG: New file. + + * cvs.texinfo (File status): Refer to "Invoking CVS", not + "status", for status options. Add paragraph about how "cvs -n -q + update" is another way to display file status. + (update examples): Removed; it had contained the "cvs -n -q + update" material. + (Invoking CVS): xref to "File status" and "Tags", not "status" and + "status options". + (status, status options): Removed. + (update options, checkout options): xref to "Invoking CVS" + not "status". + + * cvsclient.texi (Requests): Clarify how long-lived Sticky and + Static-directory are. + + * cvs.texinfo: Add @finalout. + + * cvs.texinfo (Error messages): Add "cannot change permissions on + temporary directory" message. + +Wed Apr 23 12:53:45 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Document "add" in much more detail. + +Wed Apr 23 00:38:17 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Requests): Correct small typo (`a' for `as'). + +Tue Apr 22 14:23:32 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Protocol Notes): Expand ideas on multisite + features somewhat. Add items about the network turnarounds for + pserver authentication and for protocol negotiation. + +Mon Apr 21 08:54:48 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): Describe what to do + with Entries.Log in more detail. + + * cvsclient.texi (Responses): Say "CVS 1.9 and earlier" rather + than "pre version 1.10". The latter increases confusion by + referring to a version which doesn't exist yet. + +Mon Apr 21 01:02:53 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Responses): Document Rcs-diff. Indicate that + Patched is now deprecated in favor of Rcs-diff. + +Sun Apr 20 23:42:03 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): Add note about format + of timestamp and the "Result of merge" concept. + +Sat Apr 19 13:42:33 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): It is OK for Copy-file to implement + a rename instead of a copy. + +Fri Apr 18 12:05:48 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Assigning revisions): Say that -r implies -f. + +Thu Apr 17 16:34:14 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (From other version control systems): Add comment + about CMZ and PATCHY. + +Wed Apr 16 12:35:25 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add paragraph describing how + Copy-file relates to Merged. + (Responses): Add paragraph about how it is the server which + worries about not clobbering the user's file. + +Tue Apr 15 00:57:31 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: Add notes on keyword expansion. + + * cvs.texinfo (Rename by copying): Comment out seemingly erroneous + text regarding the revision number that the new file starts with. + +Mon Apr 14 12:37:35 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Clients should try to send + notifications right away. + + * cvsclient.texi (Requests): For Notify request, clarify a few + future expansion situations. Specify the format of the time. + + * cvsclient.texi (Requests): Clarify that arguments to co, rdiff, + and rtag are module names (and how that differs from file/directory + names). + + * cvsclient.texi (Responses): Say that servers need to create + directories one at a time. + +Sat Apr 12 09:32:58 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Committing your changes): Say that editor default + is notepad (not vi) for Windows NT/95. Be more clear about what + "cvs commit" does. Add paragraph about timestamps. + (Environment variables, Global options, editinfo): + Add xrefs to that node. + +Thu Apr 10 15:48:39 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add "could not patch; will refetch". + +Wed Apr 9 15:21:11 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Working directory storage): New node. + + * cvs.texinfo (Error messages): Add comment about "cvs co ." on + NT. + +Tue Apr 8 14:44:26 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Add diff3 usage message. + +Sun Apr 6 19:03:01 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Removing files): Add comment about undoing a "cvs + remove". + + * cvsclient.texi (Requests): Explicitly mention the idea of + deferring "Notify" requests. + +Tue Apr 1 07:51:38 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add paragraph about directory + creation and empty directories. + + * cvs.texinfo (Binary files): Add comment about binary files and + merges. + + * cvsclient.texi (Requests): Add discussion of when to send + Is-modified. + + * cvsclient.texi (Requests): Sending Is-modified is enough to + prevent the file from being considered "lost". + +Sun Mar 30 00:31:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Add Is-modified request. Clarify + order of Entry relative to Unchanged or Is-modified (might as well + specify the same thing vis-a-vis Modified while we are at it). + +Sat Mar 29 12:32:40 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi: Change "newline" to "linefeed". Most of the + document already reads "linefeed" and that is what is intended. + (File transmissions): New node, moved here from Requests. + (Goals, Filenames, File transmissions, new node Strings): Add + discussion of character sets and what we expect from the transport + protocol we run on. + + * cvsclient.texi (Requests): Add paragraph about each Directory + request specifying a new local-directory and repository. + + * cvsclient.texi (Requests): Add paragraph about renaming + local-directory in Directory request. Use "local-directory" + consistently instead of "working directory", for clarity. + +Fri Mar 28 13:59:59 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Make it clear that there is no + guarantee that one will get Clear-sticky instead of another + response. Also clarify that clients will tend to store the + repository in a long-term way. + + * cvsclient.texi (Requests): Further clarify Directory example. + + * cvsclient.texi (Requests): Add example and further explanation + of what expand-modules is for. + + * cvsclient.texi (Requests): Add example, hopefully making it + clearer what REPOSITORY and LOCAL-DIRECTORY mean to Directory. + + * cvs.texinfo (Attic): New node. + (rtag options): Adjust discussion of -a accordingly. + (Repository files): Adjust accordingly. + +Thu Mar 27 09:57:05 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): Give exact wording of broken pipe + error message. + + * cvs.texinfo (history database): Add comment about various + problems with the history file. + + * cvs.texinfo (Common options): The ISO8601 web page we had + mentioned in a comment is no more. Replace it with a new one. + + * cvs.texinfo (Common options): "cvs history" also outputs dates. + +Wed Mar 26 10:54:21 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): "cvs editors" also outputs dates. + + * cvs.texinfo (Outside): Fix paragraph which said that revision + numbers start at 1.0. First of all, it is 1.1. Second of all, it + is sometimes 2.1, 3.1, etc. Third of all, the xref should be to + Assigning revisions not commit options. + + * cvs.texinfo (Outside): Comment out sentence which incorrectly + stated that "cvs add" can operate on "foo/bar.c". + +Tue Mar 25 22:21:29 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Error messages): New node. + (Magic branch numbers): Move from Troubleshooting to Revisions and + branches. The former placement never made any sense to me. + (Revision numbers): Remove "Main trunk (intro)" index entry now + that this node is right next to the other "main trunk" index + entry. + (BUGS): Very briefly mention reporting bugs in CVS. + + * cvs.texinfo (Compatibility): Add comment about "Nfoo" in CVS/Tag. + +Mon Mar 24 13:50:24 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Creating a branch): Add comment about -r in branch + example. + + * cvsclient.texi (Responses): Discuss meaning of tagspec and + future expansion in Set-sticky. The behavior described is the one + which CVS has always implemented. + +Fri Mar 21 14:19:05 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Revise meaning of "Case" per change + to CVS. + +Tue Mar 18 15:50:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + The following reorganization hopefully presents numeric revisions + in a slightly more coherent fashion. The only new material is the + paragraph about assigning revisions for added files. + * cvs.texinfo (A sample session): Bring in a sentence from Basic + concepts node, defining a repository. + (Revisions and branches): Renamed from Branches (it has always + covered non-branch tags too). Bring in nodes "Revision numbers" and + "Versions revisions releases" from Basic concepts, the former in + particular was way too detailed for an intro section. + (A sample session): Add comment about how we need an introduction + and what might go into one. Also bring in the paragraph from + Basic concepts introducing modules, but comment it out. + (Viewing differences): Add comment about + (Basic concepts): Removed; its content has been farmed out as + described above, and as the comment said, it was fundamentally + flawed. + (Assigning revisions): New node. Incorporates the "New major + release number" subsubsec which was in "commit examples". Add + paragraph concerning how CVS assigns revisions on added files. + (commit options): Refer to that node under -r. + (Invoking CVS): Add comment about text for -r. + +Tue Mar 18 13:04:30 1997 Jim Meyering <meyering@totoro.cyclic.com> + + * Makefile.in: (install-info): Depend on installdirs. + +Sun Mar 16 12:37:12 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File permissions): CVSUMASK now works for RCS + files; but it is (still) awkward for client/server CVS. + +Sat Mar 15 17:41:12 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Magic branch numbers): Add comment about where this + should go. + +Thu Mar 13 09:11:36 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Credits): Fix grammatical mistake ("manual about" + -> "manual is about"). Reported by Philippe De Muyter. + +Sun Mar 9 09:06:40 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File permissions): Add comment about val-tags and + CVSUMASK. + +Sun Mar 2 12:33:26 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (From scratch): Add comment about creating + directories with add rather than import. + + * cvs.texinfo (Creating a repository): Add comment about how this + somewhat duplicates Server requirements. + + * cvs.texinfo (Connecting via rsh): Add comment about rsh + vs. remsh. Also wording fix ("incorrect" -> "inapplicable"). + + * cvs.texinfo (Outside): Add comment about renames and annotate. + + * cvs.texinfo (Server requirements): New node. + +Thu Feb 27 15:20:49 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Multiple developers): Reword section on "cvs admin + -l". As nearly as I can tell based on when it came up on info-cvs + and other contexts, people who are into reserved checkouts + generally find that cvs admin -l is OK. Add a bunch more notes + (inside @ignore) about reserved checkout implementation ideas. + +Sun Feb 23 16:12:03 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Add various additional comments + about date formats. + + * RCSFILES: Remove diff for Id and explain it in words instead. + The previous values for Id had been clobbered by keyword expansion + on the RCSFILES file itself. + +Sat Feb 22 14:16:28 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (DISTFILES): Fix typo (missing backslash). + +Fri Feb 21 23:08:38 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * RCSFILES: New file. + * Makefile.in (DISTFILES): Add RCSFILES. + +20 Feb 1997 Lenny Foner <foner@media.mit.edu> + + * cvs.texinfo (Checklist): Fix typo ("keword" -> "keyword"). + +Thu Feb 20 21:57:05 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keeping a checked out copy): Add "web" to index. + +Wed Feb 12 18:44:16 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication client, Invoking CVS): + Document "cvs logout" command. + +Tue Feb 11 20:42:45 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (commit options): Document that the -f option to + commit disables recursion. + +Sun Feb 9 13:58:59 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (diff options): Document all the options we pass + through to diff. Remove paragraph about -D sometimes meaning + --ifdef since that is no longer true. + + * cvs.texinfo (Multiple developers): Add lengthy comment about + reserved checkout design issues. + + * cvs.texinfo (Wrappers): Add paragraph about timestamps. + + * cvs.texinfo (commit options): Don't try to document what CVS 1.3 + does with -f and how recent versions differ: 1.3 is pretty old + anyway, we generally only try to document the current version, and + the way it was described here was pretty confusing. + (Environment variables): Likewise for CVSEDITOR. + + * cvs.texinfo (import output): Add index entries for symbolic + links. Add brief mention of whether behavior should be + different. Add comments on other symbolic link issues. + +Wed Feb 5 13:02:37 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Concurrency): Add comment about commit/commit + atomicity. + +Mon Feb 3 10:55:41 1997 joel boutros <nihilis@moral.addiction.com> + + * cvs.texinfo (Connecting via rsh): Fix typo (programs -> problems). + +Fri Jan 31 12:18:47 1997 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Connection and Authentication): Correct typo + (``sent'' for ``send''), and rewrite sentence for clarity. + +Fri Jan 24 10:31:57 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (File status): Change "Unresolved Conflict" to "File + had conflicts on merge" per change to CVS. + +Sun Jan 19 16:21:17 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin): Add comments about "group" and "compiled in + value". At least one info-cvs poster was confused by this. + +Thu Jan 16 17:54:51 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): It is just -t/-f which doesn't work + client/server. -k *does* (well, except for the problem with + import noted in BUGS). -m I don't know and I doubt anyone cares. + +Mon Jan 13 15:41:02 1997 Karl Fogel <kfogel@ynu38.ynu.edu.cn> + + * cvs.texinfo (Read-only access): rephrase to imply that there may + be other administrative files, besides history and locks, which + read-only users can also affect (in the future, for example, the + `passwd' file). + +Wed Jan 8 14:50:47 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in: Remove CVSid; we decided to get rid + of these some time ago. + +Wed Jan 8 09:08:36 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): Document + restriction that cvs root sent in the cvs protocol and in the + pserver authentication protocol must be identical. + +Thu Jan 2 13:30:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in, cvs.texinfo: Remove "675" paragraph; + see ../ChangeLog for rationale. + +Thu Jan 2 09:34:51 1997 Karl Fogel <kfogel@ynu38.ynu.edu.cn> + + * cvs.texinfo (Read-only access): new node. + (Repository): new menu item for above new node. + (Password authentication server): document the user-aliasing + feature. Why was this undocumented before? + +Wed Jan 1 18:12:11 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Conflicts example): Use @asis in example to prevent + starting a line with a conflict marker. This means that when + maintaining the file with CVS itself, CVS will not think there is + a conflict merely because of the conflict marker in the example. + IMHO, this is totally bogus and CVS needs a better way of figuring + out whether a conflict is resolved (see comments elsewhere in this + node), but until then.... Credit to Fred Fish for reporting the + problem. + + * cvs.texinfo (cvsignore): Add paragraph about how .cvsignore + files in the sources being imported by "cvs import" override + "-I !". Credit goes to Fred Fish for pointing out this problem. + +Thu Dec 19 12:36:46 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Credits): Update Roland Pesch email address per his + request. + +Tue Dec 17 12:57:56 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (verifymsg): In example, remove text "and reedit if + necessary"; it was copied from editinfo and doesn't apply here. + Fix syntax of if statement; remove unnecessary attempt at loop; + don't use -n with echo. Add @appendixsec at start of node. + Add note about how verifymsg cannot change log message. + (editinfo): In paragraph saying editinfo is obsolete, fix various + typos and formatting glitches. Mention -e as well as EDITOR. + (editinfo): In saying that editinfo doesn't get consulted with -m, + -F or client/server, recommend verifymsg. Remove comment which + says, in effect, "we need a feature like verifymsg". + (editinfo example): Change "verifymsg" back to "editinfo" here; + the example is of editinfo not verifymsg. + +Tue Dec 17 12:45:32 1996 Abe Feldman <feldman@cyclic.com> + + * cvs.texinfo (verifymsg): New node. + various places: Say that editinfo is obsolete, or refer to + verifymsg instead of editinfo + +Wed Dec 11 08:55:26 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Compatibility): Add comment about 1.3 and file death. + + * cvs.texinfo (update output, release output): Document "P" as + well as "U". + +Tue Dec 10 16:23:40 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Builds): Change "make" to "implement" and "build"; + in this context "make" is ambiguous. + (Builds): Add new URL of mk web page. + +Mon Dec 9 11:03:37 1996 Jim Blandy <jimb@floss.cyclic.com> + + * cvs.texinfo (Password authentication client, Environment + variables): Remove mention of CVS_PASSWORD. + +Sun Dec 8 22:38:34 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Repository files): Mention differences between RCS + files in RCS and in CVS. + (Tags): Tag names must start with a letter. + +Fri Dec 6 09:08:18 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (syntax): Expand discussion of regular expression + syntax. + +Fri Nov 29 09:06:41 1996 fnf@ninemoons.com (Fred Fish) + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo, cvsclient.texi: Make sure @ref and friends are + followed by "," or "." as described in the texinfo manual. This + is a dubious practice as texi2html and texinfo.tex don't require + it, and makeinfo could insert them as needed, but since makeinfo + doesn't do that yet, cope. + + * cvs.texinfo (From files): Suggest "diff -r" rather than "ls -R" + as the way to see that the sources seem to have been imported + correctly. + (Common options): -k is also available with import. + (admin options): Fix typo ("interrested" -> "interested"). + +Mon Nov 25 10:03:56 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Add comments about two digit + years, year 2000, and ambiguous/nonexistent dates. + +Sun Nov 24 17:27:24 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (First import): Don't say what the wdiff program we + are using as an example does--that is confusing. Also don't show + untarring it--people might be familiar with cpio, ZIP, VMS BACKUP, + etc., instead of tar. + + * cvs.texinfo (Adding files): Update comment about "cvs add -m". + + * cvs.texinfo (Common options): Remove -H; -H is not a command + option. + (Global options): Also list --help and --version. Don't say that + -H gives a list of commands; it doesn't any more (directly). + + * cvs.texinfo: Add comment pointing to paper size web page. + + * cvs.texinfo (Common options): Rewrite section on date formats. + Executive summary is that RFC822 and ISO8601 are now preferred. + +Wed Nov 20 08:39:45 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Getting Notified): Add paragraph clarifying that + watches happen per user, not per working directory. + +Tue Nov 19 09:39:08 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tags): Suggest that future special tag names might + start with ".". Fix typo. + + * cvs.texinfo (Removing directories): -P is also available with + export. + (Moving directories): Rewrite first paragraph; now says that you + must use -P for the directory to disappear from working + directories. Thanks to Martin Lorentzon + <Martin.Lorentzson@emw.ericsson.se> for reporting this bug. + (various): Where we mention -P, point to Removing directories + node. + +Sat Nov 16 18:03:22 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Example): Rewrite to actually be based on a real + live example (and therefore reflect the way the protocol currently + works). Add comment about formatting of the document itself. + +Thu Nov 14 10:22:58 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Introduction): Use @ref, not @xref, after "see". + (Goals): Rewrite items about locking, about uploading in big + chunks, and about atomicity to be focused more on the protocol + than the current implementation. + (Notes): Remove this node. The attempt to describe the basic + model has pretty much been replaced by the Introduction. + The material about how to start the client is incomplete and + better left to cvs.texinfo. And the item about the lack of + SERVER_FLOWCONTROL is obsolete now that SERVER_FLOWCONTROL is the + default. + (Protocol Notes): Add comment about multisite features. + (Requirements): Use @code for requests and responses. + + * cvs.texinfo (Remote repositories): Add a few sentences defining + "client" and "server"; before we had been using the terms without + defining them. + + * cvs.texinfo (What is CVS?): Add paragraph about reporting bugs. + Reword and expand comp.software.config-mgmt description (and add + comments about other newsgroup facts). Point people at GNU list + of FTP sites rather than directly at prep.ai.mit.edu. + +Wed Nov 6 09:45:08 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tracking sources): Add comment regarding added and + removed files. + +Tue Nov 5 14:00:31 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Rename node "Invoking CVS" to "CVS commands". + Rewrite the intro and comments to reflect addition of the new + Invoking CVS. + (Invoking CVS): New node, a quick summary of each command. + (annotate): Don't list the options; refer to Invoking CVS and + Common options instead. + +Sun Nov 3 21:22:42 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Compatibility): New node, moved from ../README. + + * cvs.texinfo (Common options): Add comment about how tar manual + contains documentation for getdate date formats. + +Fri Nov 1 14:00:31 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (commit examples): Rewrite "New major release + number" section to tighten up the wording, better motivate the + discussion, and replace the term "rcs revision number" with + "numeric revision". + +Fri Oct 25 07:49:21 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (loginfo): Don't say "a la printf"; the syntax is + only vaguely similar to printf. + + * cvs.texinfo (loginfo): To get just the repository name, suggest + %{} instead of % "standing alone"; the latter is now an error. + +Tue Oct 22 13:08:54 1996 Noel Cragg <noel@gargle.rain.org> + + * cvs.texinfo (loginfo): add information on the new loginfo format + string specification. + +Mon Oct 21 17:33:44 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Builds): New node. + (What is CVS?): Refer to it. + +Sat Oct 19 14:32:21 1996 Jim Meyering <meyering@asic.sc.ti.com> + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Choosing a model): Wording/grammar fix. + +Sat Oct 19 14:32:21 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Obsolete): New node. + (Requests): Remove Repository and Lost and adjust Directory, + UseUnchanged, and other places accordingly. + (Required): Directory and Unchanged are now required. + + * cvs.texinfo (Removing files): Don't talk about modules; they are + not relevant in this context. + (Removing directories): New node. + (Common options): Refer to it instead of duplicating information. + +Fri Oct 18 11:05:06 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (First import, import): Add paragraph about the fact + that import doesn't modify the directory which it imports from. + + * cvs.texinfo (Creating a repository): Add paragraph about + resource requirements. + + * cvs.texinfo (Copying): Replace empty node with a copy of the GPL. + +Thu Oct 17 12:10:55 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Adding files): Revise comment to more accurately + reflect the functioning/nonfunctioning status of cvs add -m. + + * cvs.texinfo (Reverting local changes): New node, somewhat based + on the version of this node from 30 Sep 96 change. + (admin options): Refer to it. + + * cvs.texinfo: Reinstate 30 Sep 96 change from A4 to US letter. + + * cvs.texinfo (Concurrency): When telling people how to clean up + locks, tell them to make sure the locks are owned by the person + who has the stale locks. + (update output, release output): Remove text about how CVS doesn't + print "? foo" for directories; CVS has since been changed (see + conflicts-130 in sanity.sh). + +Wed Oct 16 15:01:42 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (history options): Mention new option -x E. + +Mon Oct 14 15:21:25 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tags): Add paragraph on choosing a convention for + naming tags. + +Thu Oct 10 16:05:26 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (modules): Describe what & does. + +Mon Oct 7 17:20:11 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Removing files): Correct apparent cut and paste + error: refer to the removed file, not the added file. + +Tue Oct 1 14:15:33 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Revert all recent changes (the last unscathed one + is the CVSUMASK one from Sunday). For the most part said changes + are for new features which are not appropriate at this stage of + the release process. None of the changes being reverted need to + go into 1.9, that is for sure. + +Mon Sep 30 18:17:34 1996 Greg A. Woods <woods@most.weird.com> + + * cvs.texinfo (Credits): add comment asking if we should update. + Add more detail about printing Letter on A4. + Add some comments about internal comments. + (From files): describe "cvs import -b 1" for importing existing + projects onto the main branch. + (First import): add a couple of helpful hints about naming vendor + and release tags, etc., and regularize the examples with this. + (Tracking sources): noted some reasons why you might use vendor + branches with "cvs import". + (Update imports): mention using "update" in place of "checkout" if + you have an existing working directory. + (Binary files in imports): add sub-menu separator comment. + (Tracking sources): new menu entry "Reverting to vendor release". + (Reverting to vendor release): new node to describe reverting + local changes and optionally using patch(1) to move local changes + forward. + (Global options): describe -D and -g, as well as DIFFBIN and + GREPBIN. + (export examples): add one. + (import options): describe the effect of '-b 1'. + +Mon Sep 30 08:09:53 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Adjust comments concerning A4 vs. US letter, + referring to ../README. + + * cvs.texinfo (Common options): Add comment about dates which CVS + uses in output. + +Sun Sep 29 11:14:16 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list): Don't mention Name twice. + + * cvs.texinfo (File permissions): Expand CVSUMASK stuff a bit. + (Setting a watch, Environment variables, Global options): Update + index entries for "read-only files, and ...". + + * cvsclient.texi (Requests): State that Gzip-stream is preferred + to gzip-file-contents. Cite RFC1952/1951 rather than just "gzip". + Say that RFC1950/1951 compression is also known as "zlib". + +Sat Sep 28 09:31:45 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Repository): Move all information about the + internal structure of the repository to User modules node. Rename + it to "Repository storage" ("User modules" wasn't particularly + clear). Mention CVSUMASK. Much clarification and + reorganization. + (Basic concepts): Remove material which duplicates what is now in + Repository. Rewrite paragraph introducing modules. + + * cvs.texinfo (Starting a new project): In discussing difficulty + in renaming files, don't refer to "cvs 1.x"--there is no + non-vaporous "cvs 2.x". Reword to reflect that part of the reason + to avoid renames (where possible) is not because of CVS at all, and + to try to give a general impression of how bad CVS issues involved in + renaming are. + +Fri Sep 27 04:23:44 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Adding files): Talk about directories, not modules, + since that is what is meant. Suggest using -kb option to add + rather than running cvs admin after the fact and xref to Binary + files not admin examples. Incorporate information which had been + in "add" node (there was a lot of duplication). Don't document + use of "add" on a directory to take the place of "cvs update -d"; + the latter is simpler and more logical. + (add, add options, add examples): Removed. + (release output, release options): Update xrefs accordingly. + (Adding files, Removing files): Mention the fact that adds and + removes are branch-specific. + (Merging adds and removals): New node. + + * cvs.texinfo (Concurrency): When mentioning RCS locks, use the + term reserved checkouts and xref to the place where we discuss + them in more depth. + +Thu Sep 26 08:26:01 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (log): Add comments about timezones. + (log, Common options): Add index entries for timezone and zone, time. + +Wed Sep 25 11:05:30 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (log options): Add xref to where we describe the + date formats that -d accepts. + (Common options): Don't refer to date formats accepted by co(1); + CVS's rules have never been the same. Add long whiny comment + about what a mess date formats are. + +Tue Sep 24 11:49:02 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (From other version control systems): The RCS file + must not be locked when you copy it to the CVS repository. + + * cvs.texinfo (Editing files): Also discuss how to revert in the + non-watch case. Add some index entries. + + * cvs.texinfo (update output): Add comment about how we *should* + be handling .# files. Mention fact that it is different under + VMS. Add .# to index. + +Fri Sep 20 13:08:33 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Multiple developers): Revise text on reserved + versus unreserved checkouts extensively. Move index entries for + "reserved checkouts" and "RCS-style locking" to here. Add + cross-reference to cvs admin -l. Add new section "Choosing a + model". + (Editing files): Add note about use of the word "checkout". + +Tue Sep 17 00:54:57 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Defining the module): Don't suggest "cvs co + modules"; that depends on a "modules" module being defined which + is not the default which is created by "cvs init". Instead + suggest "cvs co CVSROOT/modules" which should always work. + +Tue Sep 17 00:43:49 1996 VaX#n8 <vax@linkdead.paranoia.com> + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Rename by copying): Suggest "cvs tag -d" on the file + "new", not on everything. Also don't suggest deleting branch tags. + +Tue Sep 17 00:34:39 1996 David A. Swierczek <swierczekd@med.ge.com> + + * Makefile.in (install-info): Note whether files are in srcdir and + deal with it rather than cd'ing into srcdir. + +Mon Sep 16 23:33:36 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Add comment about using wrappers to + compress files in the repository. + + * cvs.texinfo (modules): Add comments about how we should be + documenting how -i and friends operate in client/server CVS. + + * cvs.texinfo (File permissions): Describe the need for write + permissions for locks and val-tags. + + * cvs.texinfo (commitinfo): Add comment about using commitinfo to + enforce who has access. + +Wed Jul 24 17:01:41 1996 Larry Jones <larry.jones@sdrc.com> + and Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (checkout): Refer to "update output" node. + (import): Add new import output node. + (release): Correct release output menu entry (used to be + release options instead). + (update output): Say this is output from checkout as well as + update. + +Mon Sep 16 16:18:38 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Common options): Clarify that CVS uses MM/DD/YY dates. + + * cvs.texinfo (Common options): Add comment about what HEAD means. + +Mon Sep 16 10:52:04 1996 Norbert Kiesel <nk@col.sw-ley.de> + + * cvs.texinfo (Global options): Document global '-T' option. + +Sat Sep 14 10:46:58 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keeping a checked out copy): New node. + +Fri Sep 13 23:55:42 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Magic branch numbers): Delete song and dance about + how cvs log can't cope with magic branches because rlog doesn't + know about them; cvs log no longer calls rlog. Delete item about + how you can't specify a symbolic branch to cvs log; that is fixed. + +Wed Sep 11 22:48:21 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication server): Add comments + regarding port numbers and troubleshooting. + +Tue Sep 10 10:36:00 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (What is CVS?): Reword text regarding info-cvs, + to avoid overfull hbox. + + * cvs.texinfo (Binary files): Add comment about further issues + with recovering from failure to use -kb. + + * cvs.texinfo (Conflicts example): Describe the "feature" by which + CVS won't check in files with conflicts. + (File status): Expand and revise to document all the possible + statuses from cvs status. Also document "Working revision" and + "Repository revision". Refer to other sections for other aspects + of cvs status. + (status options): Refer to other sections as appropriate. + (update output): Refer user to Conflicts example node. Add + comment regarding purging of .# files. + +Fri Sep 6 11:47:14 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Kerberos authenticated): Mention need for + --enable-encryption option in order to use encryption. + (Global options): Likewise, in description of -x option. + +Thu Sep 5 14:31:42 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Connecting via rsh): Discuss :ext:, :server:, and + CVS_RSH. + (Remote repositories): Mention what default is if no access method + is specified. + (Environment variables): Don't discuss CVS_RSH at length here; + rely on reference to "Connecting via rsh" node. + +Mon Aug 26 15:39:18 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Protocol Notes): When talking about having the + client know the original contents of files, suggest cvs edit as a + solution. + +Thu Aug 22 10:44:40 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Keyword list): Document Name keyword. + + * cvs.texinfo (Tags): Revise comment regarding legal tag names. + +Mon Aug 12 14:58:54 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication security): Add comment + about how some of this is not pserver-specific. + +Tue Aug 6 16:48:53 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (log, log options): Update for changes to cvs log + now that it no longer invokes rlog. + +Thu Jul 25 10:10:16 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Fix typo (Kerberos-request -> + Kerberos-encrypt). + +Wed Jul 24 18:53:13 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvs.texinfo (Kerberos authenticated): Change the note that the + Kerberos connection is not encrypted. + (Global options): Add documentation for -x. + * cvsclient.texi (Protocol Notes): Remove enhancement note about + Kerberos encryption. + (Requests): Add documentation for Kerberos-encrypt request. + +Thu Jul 18 18:27:40 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Creating a repository): Mention need to be able to + create lock files in the repository. + + * cvsclient.texi (Responses): In F response, make at least a + minimal attempt to define "flush". + + * cvs.texinfo (Wrappers): Document -k. + (From files, Binary files in imports): Say that imports can deal + with binary files and refer to Wrappers node for details. + (Binary files): Likewise for imports and adds. + +Sat Jul 13 18:29:10 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Binary files): Add paragraph concerning the fact + that the keyword expansion mode is not versioned, and why this is + a problem. + +Fri Jul 12 18:55:06 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Requests): Document Gzip-stream. + +Thu Jul 11 21:51:45 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvsclient.texi (Responses): Document new "F" response. + +Wed Jul 10 18:46:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (log): Don't document "rlog"; it is deprecated. + +Sat Jul 6 22:07:45 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Environment variables): Document more temp + directory nonsense, this time with "patch". + +Fri Jul 5 23:27:40 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add comment regarding "/." ending. + +Fri Sep 13 10:52:09 1996 Greg A. Woods <woods@clapton.seachange.com> + + * cvs.texinfo: don't force afourpaper -- Letter prints much better + on A4 than the other way around, believe you me! + (rdiff options): describe -k and new -K. + (RCS keywords): add description of $Name. + (Using keywords): add description of #ident and example of using + $Name. + - also fixed cross references to Substitution modes in various + places. + (import options): mention that -b 1 imports to the trunk. + +Tue Jul 2 22:40:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Sticky tags): Update to reflect change in + "resurrected" message. + +Fri Jun 28 10:48:33 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Connecting via rsh): Add comment about what we + might be saying about troubleshooting. + +Sun Jun 23 10:07:45 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication security): Add comment + regarding anoncvs as practised by OpenBSD. + +Wed Jun 19 15:41:11 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Administrative files): Add xref to Intro + administrative files. + (Intro administrative files): Add comment suggesting future + reorganizations of this material. + (syntax): Add comment regarding this node. + (Getting Notified): Actually document the notify file. It hadn't + really been documented to speak of. + (editinfo,loginfo,rcsfino,cvsignore): Make the index entries + follow the standard "foo (admin file)" format. + +Fri Jun 14 18:14:32 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (editinfo): Discuss the way editinfo falls down in + the face of -m or -F options to commit, or remote CVS. + +Thu Jun 13 15:08:27 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Watches): Add comment discussing the + fact that using cvs edit instead of chmod is not enforced. + + * cvs.texinfo (Setting up): Add index entry for "init (subcommand)". + (Creating a repository): Move contents of node Setting up here... + (Setting up): ...and remove this node. + (Creating a repository): Don't refer to the INSTALL file (it just + refers back to us!). + + * cvsclient.texi (Responses): Document the fact that the server + should send data only when the client is expecting responses. + +Wed Jun 12 16:04:48 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Entries Lines): Add comment regarding specifying + the meaning of "any other" data, in the conflict field. + (Example): Make it clear that using a separate connection for each + command is not required by the protocol. Add some comments + regarding ways in which the example is out of date or wrong. + +Fri Jun 7 18:02:36 1996 Ian Lance Taylor <ian@cygnus.com> + and Jim Kingdon <kingdon@cyclic.com> + + * cvs.texinfo (annotate): Document new -r, -D, and -f options. + +Fri Jun 7 16:59:47 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Invoking CVS): Add comment describing why only some + commands are listed here. + (Structure, Environment variables): Don't describe CVS as a + front-end to RCS. + +Tue Jun 4 21:19:42 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Document Created and Update-existing. + +Mon Jun 3 17:01:02 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Clarify "diff -c" versus "diff -u" + format in Patched response. Don't specify how the client must + implement its patch-applying functionality. + +Sun May 26 17:12:24 1996 Norbert Kiesel <nk@col.sw-ley.de> + + * cvs.texinfo (tag options) Document option "-c". + +Thu May 23 21:11:56 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Credits): Rewrite section on FAQ to reflect the + fact that FAQ is no longer maintained. + (What is CVS?): Mention comp.software.config-mgmt as well as + info-cvs. Mention the fact that info-cvs-request can be slow in + responding. + (What is CVS?): Rather than say that cvs is not a configuration + mangement system, say specifically what it lacks (change control, + etc.). I added process control (which was sorely lacking from the + list of configuration management functionality), and deleted some + functions such as tape construction which are not provided by the + well-known configuration management systems. + + * cvs.texinfo (checkout options): Add comment regarding + subdirectories (lack of clarity pointed out by ian@cygnus.com). + Add comment about that infernal "short as possible" wording. + + * cvs.texinfo (Global options): Fix error ("diff" -> "log") + (reported by ian@cygnus.com). + Remove footnote "Yes, this really should be fixed, and it's being + worked on"--it isn't clear what "this" is, and I doubt anyone is + working on it. + +Tue May 21 17:22:18 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Clarify Directory with "." as local + directory, and that filename for Questionable cannot contain "/". + +Mon May 20 13:15:25 1996 Greg A. Woods <woods@most.weird.com> + + * cvs.texinfo (rdiff): description from main.c:cmd_usage + (rtag): description from main.c:cmd_usage + (status): description from main.c:cmd_usage + (tag): description from main.c:cmd_usage + [all for the sake of consistency] + +Fri May 17 11:42:46 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Add index entries for :local:, etc. + (Password authentication server): Revert erroneous change + regarding the format of CVSROOT/passwd file. + +Thu May 16 17:06:46 1996 Noel Cragg <noel@gargle.rain.org> + + * cvsclient.texi (Notes): Removed paragraphs about various server + invocations which are now described in full in node "Connection + and Authentication." + (Requests): Include a note that "gzip-file-contents" doesn't + follow the upper/lowercase convention and that unknown reqests + always elicit a response, regardless of capitalization. + + * cvs.texinfo (Kerberos authenticated): Removed bogus version + number. + (Repository): explain the ":local:" access method. + +Wed May 15 23:43:04 1996 Noel Cragg <noel@gargle.rain.org> + + * cvsclient.texi (Goals): mention access methods. + (Requests): add note about convention: requests starting with a + captial letter don't have any expected response. Made sure each + request has a "Response expected" note. + + * cvs.texinfo (Remote repositories): add info about access + methods; fix pserver info. + +Tue May 14 08:56:41 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Environment variables): Try to document somewhat + more accurately where we put temporary files. + + * cvs.texinfo (From files): Say directory tree instead of module + where that is what we mean. Use @var{wdir} and @var{rdir} in the + example instead of using @var{dir} for two different things. + (From files): Say directory tree instead of module + where that is what we mean. + (Binary files): When using cvs admin -kb, one needs an extra + commit step on non-unix systems. + (Binary files in imports): New node. + (Wrappers): Add comment regarding indent example. + (Top): Don't refer to modules when that is not what we mean. + +Fri May 10 09:39:49 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Sticky tags): Explain what sticky dates and + non-branch sticky tags are good for. + + * cvs.texinfo (Repository): Document that -d overrides CVS/Root. + +Wed May 1 15:38:26 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tags): Document un-revision of all-uppercase tag + names. + +Wed Apr 24 08:41:51 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication security): Rewrite sentence + on complex and unknown security bugs to clarify that it is + referring to people who have been give access to cvs, not to holes + in the authentication method (which is relatively simple). + +Tue Apr 23 09:31:29 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Talk about what -m does (and does not + do). Other minor edits. + +Wed Apr 17 15:27:03 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (rcsinfo): Rewrite paragraph concerning remote CVS. + * cvsclient.texi (Responses): Document Template response. + +Sun Apr 14 16:01:39 1996 Karl Fogel <kfogel@floss.red-bean.com> + + * .cvsignore: added CVSvn.texi. + +Wed Apr 10 16:56:21 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (~/.cvsrc): Mention setting global options with "cvs". + + * cvs.texinfo (release): Change "modules" to "directories". + Release does not take module names as arguments. + + * cvs.texinfo (Creating a branch): Add comments about how we + should better document tagging the branchpoint. + +Tue Apr 9 19:59:45 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Top): Use @value{CVSVN}, not a vague refenece to 1.4. + + * cvs.texinfo (From other version control systems): New node. + +Mon Apr 8 15:59:37 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): Revise kerberos + and pserver sections to reflect the fact that port 2401 is now + officially registered. + +Thu Mar 28 09:51:13 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (History browsing): Reinstate this node. Try to get + it into some minimally useful state (it still needs a lot of + work). + (annotate): New node, subnode of History browing. + + * cvsclient.texi (Requests): Add annotate request. + +Tue Mar 26 08:46:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: In various examples, change tag names to avoid tag + names reserved to CVS. + + * cvs.texinfo (Tags): Document what is a valid tag name. + + * cvs.texinfo (Substitution modes): Try to describe how the + various keyword expansion settings interract. + (Binary files): Suggest cvs update -A, not removing file and then + updating it, to get effect of new keyword expansion options. + + * cvs.texinfo (admin options): Mention CVS's use of `dead' state. + +Thu Mar 21 08:25:17 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Environment variables): Expand introduction to RCS + environment variables. Expand and correct CVS_SERVER_SLEEP. + + * cvs.texinfo (Environment variables): Remove POSIXLY_CORRECT; cvs + requires options to precede arguments regardless of it. + +Thu Mar 21 08:18:42 1996 Norbert Kiesel <nk@col.sw-ley.de> + + * cvs.texinfo: Remove paragrahps about a forthcoming CVS + newsgroup and about sending patches to think.com. + (Environment): Document some more (all?) used environment + variables. + +Wed Mar 20 09:44:21 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Introduction): New node. + * Makefile.in: Add cruft to reflect fact that cvsclient.texi now + uses CVSvn.texi. + +Mon Mar 18 14:43:53 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Add Case request. + +Wed Mar 13 16:01:47 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Connection and Authentication): New node. + + * cvsclient.texi (Requests): Expand discussion of Root a bit. + + * cvs.texinfo (Setting up): Don't refer to INSTALL file; revise to + reflect some information which had been in the INSTALL file. + + * cvs.texinfo (history file): Update to reflect cvsinit -> cvs + init. Adjust discussion of whether history file format is + documented. + (Setting up): Update to reflect cvsinit -> cvs init. + + * cvsclient.texi (Requests): Document init request. + +Thu Feb 29 10:08:31 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (loginfo example): Adjust example to reflect the way + that CVS actually works. Add comments questioning whether that is + the best behavior. + + * cvs.texinfo (cvsignore): Document additions to default ignore list. + +Mon Feb 26 13:48:01 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Filenames): New node, documents / vs \, etc. + +Wed Feb 24 1996 Marcus Daniels <marcus@sayre.sysc.pdx.edu> + + * cvs.texinfo (Password authentication server): Mention + support for imaginary usernames. + +Thu Feb 15 16:34:56 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Variables): Add new internal variable $USER. + +Wed Feb 14 22:52:58 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (export, admin): Document -k option to cvs export. + + * cvs.texinfo (admin options): Mention using -l, -u, -L, and -U in + conjunction with rcslock.pl. + +Mon Feb 12 16:38:41 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Remove references to mkmodules. + +Sun Feb 11 12:31:36 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi: Add Set request. + + * cvs.texinfo (Variables): Rewrite to reflect user variables + replacing environment variables; motivate the discussion better. + (Global options): Add -s option. + +Sat Feb 10 11:18:37 1996 Jim Blandy <jimb@totoro.cyclic.com> + + * cvs.texinfo (Variables): Fix @table commands. + +Fri Feb 9 17:31:18 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Variables): New node. + + * Makefile.in (CVSvn.texi): New rule. + (OBJDIR_DISTFILES): Add CVSvn.texi. + (cvs.dvi,cvs.info): Add cruft to deal with it being in build dir + or srcdir. + * cvs.texinfo: Include CVSvn.texi and use the version number from + it instead of a hardcoded version number and date. + +Thu Feb 1 13:28:03 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Sticky tags): Expand so it really documents the + features it is talking about rather than referring to "Appendix + A". Add example of how to restore the old version of a dead + file. In various other parts of the manual refer to this node, in + some cases deleting duplicative text. In the case of cvs admin + -b, mention vendor branch usage. + (Removing files): Discuss removing files (in user-visible terms, + not in terms of the Attic and such). + (remove): Remove node; merge contents into Removing files. + +Tue Jan 30 17:52:06 1996 Jim Blandy <jimb@totoro.cyclic.com> + + * cvs.texinfo: Tweak @top node, to make file compatible with both + makeinfo and texinfo-format-buffer. Perhaps we should fix the + formatters to agree on what constitutes valid texinfo. + +Mon Jan 29 16:38:33 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requirements): New node, to talk about required + versus optional parts of the protocol. + +Sun Jan 28 09:00:34 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Modes): Add discussion what what the mode really + means (across diverse operating systems). + +Tue Jan 23 12:54:57 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Per mail from Per Cederqvist, change author to "Per + Cederqvist et al". Also remove sentence about Signum shipping + hardcopy manuals and add information on Cyclic. Change version + number to 1.6.87. + +Fri Jan 12 15:29:39 1996 Vince Demarco <vdemarco@bou.shl.com> + + * cvs.texinfo: Fix the documentation for the com/uncom change + to wrap/unwrap. make everything consistant + +Wed Jan 10 16:11:54 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Concurrency): Add index entries; minor clarification. + +Tue Jan 9 16:03:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Getting Notified): Document users file. + + * cvs.texinfo (cvsignore): Add *.obj to list of ignored files. + +Wed Jan 3 17:01:58 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (import): Adjust list of ignored files to match + recent change to CVS (CVS* -> CVS CVS.adm). Consolidate + discussion of ignored files in one place (with xrefs from others). + + * cvsclient.texi: Remove How To node. It was out of date + (again!), and I am getting sick of trying to update it (internals + documentation should be in the comments, where it at least has a + fighting chance of staying up to date). + (Protocol): Say what \n and \t mean in this document. + +Tue Jan 2 23:39:32 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Wrappers): Change comb/uncom to wrap/unwrap. + +Mon Jan 2 23:00:00 1996 Vince Demarco <vdemarco@bou.shl.com> + + * cvs.texinfo: update the Wrappers documentation so it isn't + so NEXTSTEP centric. The wrappers code has alot of other + general uses. The new version of the documentation tryes + to show that to the reader. + +Mon Jan 1 13:09:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Clarify that Module-expansion is not + suitable for passing to co. + +Sun Dec 31 10:53:47 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Password authentication server): Suggest specifying + -b in inetd.conf. + + * cvs.texinfo (Password authentication): Variety of cleanups and + minor fixes, including shorter node names. + +Sun Dec 24 02:37:51 1995 Karl Fogel <kfogel@floss.cyclic.com> + + * cvs.texinfo (Using the client with password authentication): + tixed fypos. + +Sun Dec 24 00:00:16 1995 Karl Fogel <kfogel@floss.cyclic.com> + + * cvs.texinfo (Remote repositories): use @code{rsh} most places, + because it is the name of a program, and because I am a pedant. + Refer to new node "Password authenticated". + (Password authenticated): new node. + (Setting up the server for password authentication): new node. + (Using the client with password authentication): new node. + (Security considerations with password authentication): new node. + + These are all really long node names, but it seems necessary that + they be descriptive in case they're referenced elsewhere. If you + can think of a way out of this, please change them. + +Thu Dec 21 12:09:34 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Requests): Add Questionable. Revise + documentation of export and update to explain role of -I option. + +Tue Dec 19 16:44:18 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Update binary files info for -kb. + +Mon Dec 11 12:20:55 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Responses): Add Notified and Mode. + (Requests): Add Notify, noop, watch-on, watch-off, watch-add, + watch-remove, watchers, and editors. + * cvs.texinfo (Watches): New node, to describe new developer + communication features. + +Thu Nov 23 08:59:09 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (admin options): In saying that cvs admin -o is not + such a good way to undo a change, refer to the section which + describes the preferred way. + +Thu Nov 13 16:39:03 1995 Fred Fish <fnf@cygnus.com> + + * Makefile.in: Remove extraneous tab from empty line. + +Mon Nov 13 15:00:26 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Concurrency): New node, to describe user-visible + behaviors associated with cvs locks. + + * cvs.texinfo (Remote repositories): Add more details of how to + set things up (with rsh and kerberos). + +Thu Nov 9 11:41:37 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Remove -Q and -q options from command synopses. + +Wed Nov 8 09:38:00 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (Notes): Revise paragraph on server memory use + problem. + +Tue Nov 7 16:26:39 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Document merging more than once from a branch; + miscellaneous cleanups. + +Mon Oct 30 13:12:53 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (modules): Document -e. + +Thu Oct 26 11:15:40 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Tags): Update "version" vs. "revision" for CVS 1.5. + (Index,BUGS): Change bug reporting address from Per Cederqvist to + bug-cvs@prep.ai.mit.edu. + +Wed Oct 25 15:37:05 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Miscellaneous minor changes (clean up CVS/Root + stuff, don't say release requires a module entry, etc.). + +Tue Oct 24 11:01:22 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: More precisely describe scope of document. + * cvsclient.texi: Describe scope of document + +Thu Oct 12 11:25:40 1995 Karl Fogel <kfogel@totoro.cyclic.com> + + * cvs.texinfo: cover page now refers to CVS 1.6, and "last + updated" date has been upped to today. + +Wed Oct 11 22:30:10 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (info): Look for *.info* either in build dir or in + srcdir. + +Mon Oct 2 17:10:49 1995 Norbert Kiesel <nk@col.sw-ley.de> + + * cvs.texinfo (admin): Describe usage of CVS_ADMIN_GROUP to + restrict usage of admin. + +Fri Oct 6 21:17:50 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (~/.cvsrc): Document change to command name matching. + +Thu Oct 5 18:03:41 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (install-info): Add comment about srcdir. + +Wed Sep 13 12:45:53 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Moving files): Rewrite "Outside" node to clarify + that history is still there and describe how to get it. Assorted + cleanups. + +Tue Sep 12 19:02:47 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo (Removing files): Remove section on limitations + which are gone now that we have death support. + +Wed Aug 30 12:32:29 1995 Karl Fogel <kfogel@floss.cyclic.com> + + * cvs.texinfo (Remote Repositories): new node, referred to from + `Basics' and `Repository'. + (Repository): documented new `-d' vs. `$CVSROOT' vs. `CVS/Root' + behavior. + (commitinfo): document client/server-case behavior. + (editinfo): document client/server-case behavior. + (loginfo): document client/server-case behavior. + (rcsinfo): document client/server-case behavior. + +Mon Aug 21 00:23:45 1995 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsclient.texi (How To): The way to force rsh is to set + CVS_CLIENT_PORT to -1, not to some bogus value. + +Tue Aug 15 17:12:08 1995 Karl Fogel <kfogel@floss.cyclic.com> + + * cvs.texinfo + (Basic concepts): talk about remote repositories. + (Repository): same. + +Mon Jul 24 19:09:12 1995 James Kingdon <kingdon@harvey.cyclic.com> + + * cvs.texinfo: Remove references to -q and -Q command options. + +Fri Jul 21 10:33:07 1995 Vince DeMarco <vdemarco@bou.shl.com> + + * cvs.texinfo: Changes for CVSEDITOR and wrappers. + +Thu Jul 13 23:04:12 CDT 1995 Jim Meyering (meyering@comco.com) + + * Makefile.in (cvs-paper.ps): *Never* redirect output directly to + the target (usu $@) of a rule. Instead, redirect to a temporary + file, and then move that temporary to the target. I chose to + name temporary files $@-t. Remember to be careful that the length + of the temporary file name not exceed the 14-character limit. + +Sun Jul 9 19:03:00 1995 Greg A. Woods <woods@most.weird.com> + + * doc/cvs.texinfo: + - document '-q' for 'cvs status' + - correction to regexp use in *info files + - correction to use of 'cvsinit' script + (from previous local changes) + +Tue Jun 20 18:57:55 1995 James Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (dist-dir): Depend on $(OBJDIR_DISTFILES). + +Fri Jun 16 21:56:16 1995 Karl Fogel <kfogel@cyclic.com> + and Jim Meyering <meyering@comco.com> + + * update.c (update_file_proc): If noexec, just write 'C', don't merge. + +Fri Jun 16 07:56:04 1995 Jim Kingdon (kingdon@cyclic.com) + + * cvs-paper.ps: Added. + +Sat May 27 08:46:00 1995 Jim Meyering (meyering@comco.com) + + * Makefile.in (Makefile): Regenerate only Makefile in current + directory when Makefile.in is out of date. Depend on ../config.status. + +Sat May 27 08:08:18 1995 Jim Meyering (meyering@comco.com) + + * doc/Makefile.in (realclean): Remove more postscript and info files. + +Fri Apr 28 22:44:06 1995 Jim Blandy <jimb@totoro.bio.indiana.edu> + + * Makefile.in (DISTFILES): Updated. + (doc): Depend on cvsclient.ps too. + (cvs.aux, cvsclient.aux): Add target. + (cvsclient.dvi): Don't nuke the aux file. They're small and + helpful. + (cvsclient.ps): New target. + (dist-dir): Renamed from dist; changed to work with DISTDIR + variable from parent. + +Sun Apr 23 22:13:18 1995 Noel Cragg <noel@vo.com> + + * Makefile: Added more files to the `clean' target. + * .cvsignore: Added the same files. + +Mon Nov 28 10:22:46 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Notes): Remove item about commit options; now + fixed. Rewrite paragraph about server memory usage. + + * cvsclient.texi (Responses): Add Set-checkin-prog and + Set-update-prog. + (Requests): Add Checkin-prog and Update-prog. + * cvsclient.texi (TODO): Remove last item (it is fixed) and node. + +Fri Nov 18 16:51:36 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Requests): Add Max-dotdot. + +Thu Nov 3 07:04:24 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Add Directory request. + (TODO): Remove item about renaming directories. + (Protocol): Change @subheading to @node/@section. + +Fri Oct 28 07:51:13 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Add expand-module request and + Module-expansion response. + (Protocol Notes, TODO): Remove items about cvs co funkiness. + +Wed Oct 12 19:49:36 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Add Copy-file response. + + * cvsclient.texi (How To): Correct item about where declaration + of cvs commands go. + + * cvsclient.texi (Protocol): Add new commands. Merge description + of how commands work which was duplicated among the various + commands. Formatting cleanups. + (TODO): Remove item about bad error message on checking in a + nonexistent file; this works now (presumably fixed by the + Unchanged stuff). + (Notes): Remove thing about trying unsupported commands via NFS, + rdist, etc. Also remove item about some commands not being + supported. There are no unsupported commands anymore. + +Tue Sep 13 13:28:52 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Document New-entry response. + +Mon Sep 12 06:35:15 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Clarify that checksum is of patched + file, not patch itself. Fix typo (valid-requests -> Valid-requests). + + * cvsclient.texi (Protocol): Document Sticky request and + Set-sticky and Clear-sticky responses. + (Notes): Remove sticky tags from todo list. + +Thu Sep 8 14:23:58 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Document Static-directory requests + and Set-static-directory and Clear-static-directory responses. + (Notes): Remove Entries.Static support from todo list. + + * cvsclient.texi (Protocol): Document Unchanged and UseUnchanged + requests. Update documentation of Entry and Lost accordingly. + +Mon Aug 22 14:08:21 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Goals): Remove mention of rsh. + (Protocol Notes, TODO): Remove compression item. + (Protocol): Document "status" request. + (TODO): Remove suggestion to add "cvs status". + +Tue Jul 19 10:02:53 1994 Ian Lance Taylor (ian@sanguine.cygnus.com) + + * Makefile.in (install-info): Do not depend upon installdirs. + +Fri Jul 15 12:56:53 1994 Ian Lance Taylor (ian@sanguine.cygnus.com) + + * Makefile.in (all): Do not depend upon info. + (install): Do not depend upon install-info. + +Thu Jul 7 20:43:12 1994 Ian Lance Taylor (ian@sanguine.cygnus.com) + + * cvsclient.texi (Protocol): Add Checksum response. + +Thu Jun 30 15:16:50 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Add Global_option request. + +Wed Jun 29 14:09:42 1994 Ian Lance Taylor (ian@sanguine.cygnus.com) + + * cvsclient.texi: Describe sending patches, including the dummy + update-patches request and the Patched response. Mention Kerberos + authentication using ``cvs kserver''. Some other minor changes. + +Tue Jun 28 15:21:06 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol Notes): Remove note about sending diffs + in Updated; Ian did it. Remove note about adding encryption to rsh. + +Sat May 7 10:44:30 1994 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi (Protocol): Document Modified without Entry. Add + `add' and `remove' and `Remove-entry'. Formatting cleanups. + +Tue Apr 19 01:29:04 1994 John Gilmore (gnu@cygnus.com) + + * cvsclient.texi: New node How To; cleanups throughout. + * Makefile.in: Add dependencies on cvsclient.texi. + diff --git a/gnu/dist/cvs/doc/ChangeLog.fsf b/gnu/dist/cvs/doc/ChangeLog.fsf new file mode 100644 index 00000000000..2f140999caf --- /dev/null +++ b/gnu/dist/cvs/doc/ChangeLog.fsf @@ -0,0 +1,38 @@ +Thu Sep 15 14:19:50 1994 david d `zoo' zuhn <zoo@monad.armadillo.com> + + * Makefile.in: define TEXI2DVI + +Sat Dec 18 01:23:39 1993 david d zuhn (zoo@monad.armadillo.com) + + * cvs.texinfo: document -k SUBST options to 'cvs import'; + regularize use @sc{cvs} + + * Makefile.in (VPATH): don't use $(srcdir), but @srcdir@ instead + (install-info): grab all info files, not just *.info + +Mon Oct 11 16:23:54 1993 Jim Kingdon (kingdon@lioth.cygnus.com) + + * cvsclient.texi: New node TODO; various other changes. + +Wed Feb 26 18:04:40 1992 K. Richard Pixley (rich@cygnus.com) + + * Makefile.in, configure.in: removed traces of namesubdir, + -subdirs, $(subdir), $(unsubdir), some rcs triggers. Forced + copyrights to '92, changed some from Cygnus to FSF. + +Tue Dec 10 04:07:10 1991 K. Richard Pixley (rich at rtl.cygnus.com) + + * Makefile.in: infodir belongs in datadir. + +Thu Dec 5 22:46:01 1991 K. Richard Pixley (rich at rtl.cygnus.com) + + * Makefile.in: idestdir and ddestdir go away. Added copyrights + and shift gpl to v2. Added ChangeLog if it didn't exist. docdir + and mandir now keyed off datadir by default. + +Wed Nov 27 02:45:18 1991 K. Richard Pixley (rich at sendai) + + * brought Makefile.in's up to standards.text. + + * fresh changelog. + diff --git a/gnu/dist/cvs/doc/Makefile.in b/gnu/dist/cvs/doc/Makefile.in new file mode 100644 index 00000000000..29eb565d46a --- /dev/null +++ b/gnu/dist/cvs/doc/Makefile.in @@ -0,0 +1,197 @@ +# Makefile for GNU CVS documentation. +# Do not use this makefile directly, but only from `../Makefile'. +# Copyright (C) 1986, 1988-1990 Free Software Foundation, Inc. + +# 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, 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. + +SHELL = /bin/sh + +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +VPATH = @srcdir@ + +prefix = @prefix@ +exec_prefix = @exec_prefix@ + +infodir = $(prefix)/info + +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ + +DISTFILES = \ + .cvsignore ChangeLog ChangeLog.fsf Makefile.in \ + RCSFILES \ + cvs-paper.ms cvs-paper.ps \ + cvs.texinfo \ + cvsclient.texi + +OBJDIR_DISTFILES = cvs.ps cvs.info cvs.aux \ + cvsclient.ps cvsclient.info cvsclient.aux CVSvn.texi + +# these are part of the texinfo distribution +MAKEINFO=makeinfo +TEXI2DVI = texi2dvi + +# where to find texinfo; +TEXIDIR=${gdbdir}/../texinfo + +SET_TEXINPUTS = TEXINPUTS=.:$(srcdir):$$TEXINPUTS + +# Don Knuth's TeX formatter +TEX = tex + +# auxiliary program for sorting Texinfo indices +TEXINDEX = texindex + +DVIPS = dvips +DVIPSFLAGS = + +ROFF = groff + +# CYGNUS LOCAL: all does not depend upon info +all: +.PHONY: all + +# CYGNUS LOCAL: install does not depend on install-info +install: all +.PHONY: install + +doc: cvs.ps cvs-paper.ps cvsclient.ps + +info: cvs.info cvsclient.info + +cvs.info: cvs.texinfo CVSvn.texi + if [ ! -f ./CVSvn.texi ]; then \ + ln -s $(srcdir)/CVSvn.texi . || \ + ln $(srcdir)/CVSvn.texi . || \ + cp $(srcdir)/CVSvn.texi . ; else true; fi + $(MAKEINFO) $(srcdir)/cvs.texinfo -o cvs.info + +cvsclient.info: cvsclient.texi CVSvn.texi + if [ ! -f ./CVSvn.texi ]; then \ + ln -s $(srcdir)/CVSvn.texi . || \ + ln $(srcdir)/CVSvn.texi . || \ + cp $(srcdir)/CVSvn.texi . ; else true; fi + $(MAKEINFO) $(srcdir)/cvsclient.texi -o cvsclient.info + +# Version of the protocol suitable for emailing +cvsclient.txt: cvsclient.texi CVSvn.texi + if [ ! -f ./CVSvn.texi ]; then \ + ln -s $(srcdir)/CVSvn.texi . || \ + ln $(srcdir)/CVSvn.texi . || \ + cp $(srcdir)/CVSvn.texi . ; else true; fi + $(MAKEINFO) $(srcdir)/cvsclient.texi --no-headers -o cvsclient.txt + +# If the user gets a distribution (which contains *.info), unpacks +# it, and builds it in a seperate build dir, then *.info* are in srcdir. +# If the user builds *.info (e.g. after editing *.texi), then *.info* are +# in the build dir. +# (Note: don't solve this problem with "cd"; INSTALL_DATA might be a +# relative path to install-sh). +install-info: info installdirs + if test -f cvs.info ; then docdir=.; else docdir=$(srcdir);fi; \ + for i in $$docdir/*.info* ; do \ + $(INSTALL_DATA) $$i $(infodir)/`basename $$i` ; \ + done + +installdirs: + $(SHELL) $(top_srcdir)/mkinstalldirs $(infodir) +.PHONY: installdirs + +dvi: cvs.dvi cvsclient.dvi + +CVSvn.texi: $(top_srcdir)/src/version.c + echo "@set CVSVN `sed < $(top_srcdir)/src/version.c \ + -e '/version_string/!d' \ + -e 's/[^0-9.]*\([0-9.]*\).*/\1/' \ + -e q`" >CVSvn.new + mv CVSvn.new CVSvn.texi + +cvs.dvi cvs.aux: cvs.texinfo CVSvn.texi + if [ ! -f ./CVSvn.texi ]; then \ + ln -s $(srcdir)/CVSvn.texi . || \ + ln $(srcdir)/CVSvn.texi . || \ + cp $(srcdir)/CVSvn.texi . ; else true; fi + $(TEXI2DVI) $(srcdir)/cvs.texinfo + +cvsclient.dvi cvsclient.aux: cvsclient.texi CVSvn.texi + if [ ! -f ./CVSvn.texi ]; then \ + ln -s $(srcdir)/CVSvn.texi . || \ + ln $(srcdir)/CVSvn.texi . || \ + cp $(srcdir)/CVSvn.texi . ; else true; fi + $(TEXI2DVI) $(srcdir)/cvsclient.texi + rm -f cvsclient.?? cvsclient.log cvsclient.toc cvsclient.??s + +cvs.ps: cvs.dvi + $(DVIPS) $(DVIPSFLAGS) cvs.dvi -o cvs.ps + +cvs-paper.ps: cvs-paper.ms + $(ROFF) -t -p -ms -Tps $(srcdir)/cvs-paper.ms > $@-t + mv $@-t $@ + +cvsclient.ps: cvsclient.dvi + $(DVIPS) $(DVIPSFLAGS) cvsclient.dvi -o cvsclient.ps + +tags: +.PHONY: tags + +TAGS: +.PHONY: TAGS + +ls: + @echo $(DISTFILES) +.PHONY: ls + +clean: + rm -f *.o core + rm -f cvs.cp cvs.fn cvs.ky cvs.pg cvs.tp cvs.vr + rm -f cvs.cps cvs.fns cvs.kys cvs.pgs cvs.tps cvs.vrs + rm -f cvs.aux cvs.dvi cvs.log cvs.toc + rm -f cvsclient.cp cvsclient.fn cvsclient.ky cvsclient.pg + rm -f cvsclient.tp cvsclient.vr cvsclient.cps cvsclient.fns + rm -f cvsclient.kys cvsclient.pgs cvsclient.tps cvsclient.vrs + rm -f cvsclient.aux cvsclient.dvi cvsclient.log cvsclient.toc + +.PHONY: clean + +distclean: clean + rm -f Makefile +.PHONY: distclean + +realclean: distclean + rm -f cvs.info* cvs.ps cvs-paper.ps cvsclient.info* cvsclient.ps +.PHONY: realclean + +dist-dir: $(DISTFILES) $(OBJDIR_DISTFILES) + mkdir ${DISTDIR} + for i in ${DISTFILES}; do \ + ln $(srcdir)/$${i} ${DISTDIR}; \ + done + ln ${OBJDIR_DISTFILES} ${DISTDIR} + if [ -f cvs.info-1 ]; \ + then ln -f cvs.info-* ${DISTDIR}; \ + else : Pacify Ultrix sh; \ + fi + if [ -f cvsclient.info-1 ]; \ + then ln -f cvsclient.info-* ${DISTDIR}; \ + else : Pacify Ultrix sh; \ + fi +.PHONY: dist-dir + +subdir = doc +Makefile: ../config.status Makefile.in + cd .. && CONFIG_FILES=$(subdir)/$@ CONFIG_HEADERS= ./config.status + +#../config.status: ../configure +# cd .. ; $(SHELL) config.status --recheck + +#../configure: ../configure.in +# cd $(top_srcdir) ; autoconf diff --git a/gnu/dist/cvs/doc/RCSFILES b/gnu/dist/cvs/doc/RCSFILES new file mode 100644 index 00000000000..46503377901 --- /dev/null +++ b/gnu/dist/cvs/doc/RCSFILES @@ -0,0 +1,276 @@ +It would be nice if the RCS file format (which is implemented by a +great many tools, both free and non-free, both by calling GNU RCS and +by reimplementing access to RCS files) were documented in some +standard separate from any one tool. But as far as I know no such +standard exists. Hence this file. + +The place to start is the rcsfile.5 manpage in the GNU RCS 5.7 +distribution. Then look at the diff at the end of this file (which +contains a few fixes and clarifications to that manpage). + +If you are interested in MKS RCS, src/ci.c in GNU RCS 5.7 has a +comment about their date format. However, as far as we know there +isn't really any document describing MKS's changes to the RCS file +format. + +The rcsfile.5 manpage does not document what goes in the "text" field +for each revision. The answer is that the head revision contains the +contents of that revision and every other revision contain a bunch of +edits to produce that revision ("a" and "d" lines). The GNU diff +manual (the version I looked at was for GNU diff 2.4) documents this +format somewhat (as the "RCS output format"), but the presentation is +a bit confusing as it is all tangled up with the documentation of +several other output formats. If you just want some source code to +look at, the part of CVS which applies these is RCS_deltas in +src/rcs.c. + +The rcsfile.5 documentation only _very_ briefly touches on the order +of the revisions. The order _is_ important and CVS relies on it. +Here is an example of what I was able to find, based on the join3 +sanity.sh testcase (and the behavior I am documenting here seems to be +the same for RCS 5.7 and CVS 1.9.27): + + 1.1 -----------------> 1.2 + \---> 1.1.2.1 \---> 1.2.2.1 + +Here is how this shows up in the RCS file (omitting irrelevant parts): + + admin: head 1.2; + deltas: + 1.2 branches 1.2.2.1; next 1.1; + 1.1 branches 1.1.2.1; next; + 1.1.2.1 branches; next; + 1.2.2.1 branches; next; + deltatexts: + 1.2 + 1.2.2.1 + 1.1 + 1.1.2.1 + +Yes, the order seems to differ between the deltas and the deltatexts. +I have no idea how much of this should actually be considered part of +the RCS file format, and how much programs reading it should expect to +encounter any order. + +The rcsfile.5 grammar shows the {num} after "next" as optional; if it +is omitted then there is no next delta node (for example 1.1 or the +head of a branch will typically have no next). + +There is one case where CVS uses CVS-specific, non-compatible changes +to the RCS file format, and this is magic branches. See cvs.texinfo +for more information on them. CVS also sets the RCS state to "dead" +to indicate that a file does not exist in a given revision (this is +stored just as any other RCS state is). + +The RCS file format allows quite a variety of extensions to be added +in a compatible manner by use of the "newphrase" feature documented in +rcsfile.5. We won't try to document extensions not used by CVS in any +detail, but we will briefly list them. Each occurrence of a newphrase +begins with an identifier, which is what we list here. Future +designers of extensions are strongly encouraged to pick +non-conflicting identifiers. Note that newphrase occurs several +places in the RCS grammar, and a given extension may not be legal in +all locations. However, it seems better to reserve a particular +identifier for all locations, to avoid confusion and complicated +rules. + + Identifier Used by + ---------- ------- + namespace RCS library done at Silicon Graphics Inc. (SGI) in 1996 + (a modified RCS 5.7--not sure it has any other name). + dead A set of RCS patches developed by Rich Pixley at + Cygnus about 1992. These were for CVS, and predated + the current CVS death support, which uses a state "dead" + rather than a "dead" newphrase. + +CVS does use newphrases to implement the `PreservePermissions' +extension introduced in CVS 1.9.26. The following new keywords are +defined when PreservePermissions=yes: + + owner + group + permissions + special + symlink + hardlinks + +The contents of the `owner' and `group' field should be a numeric uid +and a numeric gid, respectively, representing the user and group who +own the file. The `permissions' field contains an octal integer, +representing the permissions that should be applied to the file. The +`special' field contains two words; the first must be either `block' +or `character', and the second is the file's device number. The +`symlink' field should be present only in files which are symbolic +links to other files, and absent on all regular files. The +`hardlinks' field contains a list of filenames to which the current +file is linked, in alphabetical order. Because files often contain +characters special to RCS, like `.' and sometimes even contain spaces +or eight-bit characters, the filenames in the hardlinks field will +usually be enclosed in RCS strings. For example: + + hardlinks README @install.txt@ @Installation Notes@; + +The hardlinks field should always include the name of the current +file. That is, in the repository file README,v, any hardlinks fields +in the delta nodes should include `README'; CVS will not operate +properly if this is not done. + +The rules regarding keyword expansion are not documented along with +the rest of the RCS file format; they are documented in the co(1) +manpage in the RCS 5.7 distribution. See also the "Keyword +substitution" chapter of cvs.texinfo. The co(1) manpage refers to +special behavior if the log prefix for the $Log keyword is /* or (*. +RCS 5.7 produces a warning whenever it behaves that way, and current +versions of CVS do not handle this case in a special way (CVS 1.9 and +earlier invoke RCS to perform keyword expansion). + +Note that if the "expand" keyword is omitted from the RCS file, the +default is "kv". + +Note that the "comment {string};" syntax from rcsfile.5 specifies a +comment leader, which affects expansion of the $Log keyword for old +versions of RCS. The comment leader is not used by RCS 5.7 or current +versions of CVS. + +Both RCS 5.7 and current versions of CVS handle the $Log keyword in a +different way if the log message starts with "checked in with -k by ". +I don't think this behavior is documented anywhere. + +Here is a clarification regarding characters versus bytes in certain +character sets like JIS and Big5: + + The RCS file format, as described in the rcsfile(5) man page, is + actually byte-oriented, not character-oriented, despite hints to + the contrary in the man page. This distinction is important for + multibyte characters. For example, if a multibyte character + contains a `@' byte, the `@' must be doubled within strings in RCS + files, since RCS uses `@' bytes as escapes. + + This point is not an issue for encodings like ISO 8859, which do + not have multibyte characters. Nor is it an issue for encodings + like UTF-8 and EUC-JIS, which never uses ASCII bytes within a + multibyte character. It is an issue only for multibyte encodings + like JIS and BIG5, which _do_ usurp ASCII bytes. + + If `@' doubling occurs within a multibyte char, the resulting RCS + file is not a properly encoded text file. Instead, it is a byte + stream that does not use a consistent character encoding that can + be understood by the usual text tools, since doubling `@' messes + up the encoding. This point affects only programs that examine + the RCS files -- it doesn't affect the external RCS interface, as + the RCS commands always give you the properly encoded text files + and logs (assuming that you always check in properly encoded + text). + + CVS 1.10 (and earlier) probably has some bugs in this area on + systems where a C "char" is signed and where the data contains + bytes with the eighth bit set. + +One common concern about the RCS file format is the fact that to get +the head of a branch, one must apply deltas from the head of the trunk +to the branchpoint, and then from the branchpoint to the head of the +branch. While more detailed analyses might be worth doing, we will +note: + + * The performance bottleneck for CVS generally is figuring out which + files to operate on and that sort of thing, not applying deltas. + + * Here is one quick test (probably not a very good test; a better test + would use a normally sized file (say 50-200K) instead of a small one): + + I just did a quick test with a small file (on a Sun Ultra 1/170E + running Solaris 5.5.1), with 1000 revisions on the main branch and + 1000 revisions on branch that forked at the root (i.e., RCS revisions + 1.1, 1.2, ..., 1.1000, and branch revisions 1.1.1.1, 1.1.1.2, ..., + 1.1.1.1000). It took about 0.15 seconds real time to check in the + first revision, and about 0.6 seconds to check in and 0.3 seconds to + retrieve revision 1.1.1.1000 (the worst case). + + * Any attempt to "fix" this problem should be careful not to interfere + with other features, such as lightweight creation of branches + (particularly using CVS magic branches). + +Diff follows: + +(Note that in the following diff the old value for the Id keyword was: + Id: rcsfile.5in,v 5.6 1995/06/05 08:28:35 eggert Exp +and the new one was: + Id: rcsfile.5in,v 5.7 1996/12/09 17:31:44 eggert Exp +but since this file itself might be subject to keyword expansion I +haven't included a diff for that fact). + +=================================================================== +RCS file: RCS/rcsfile.5in,v +retrieving revision 5.6 +retrieving revision 5.7 +diff -u -r5.6 -r5.7 +--- rcsfile.5in 1995/06/05 08:28:35 5.6 ++++ rcsfile.5in 1996/12/09 17:31:44 5.7 +@@ -85,7 +85,8 @@ + .LP + \f2sym\fP ::= {\f2digit\fP}* \f2idchar\fP {\f2idchar\fP | \f2digit\fP}* + .LP +-\f2idchar\fP ::= any visible graphic character except \f2special\fP ++\f2idchar\fP ::= any visible graphic character, ++ except \f2digit\fP or \f2special\fP + .LP + \f2special\fP ::= \f3$\fP | \f3,\fP | \f3.\fP | \f3:\fP | \f3;\fP | \f3@\fP + .LP +@@ -119,12 +120,23 @@ + the minute (00\-59), + and + .I ss +-the second (00\-60). ++the second (00\-59). ++If + .I Y +-contains just the last two digits of the year +-for years from 1900 through 1999, +-and all the digits of years thereafter. +-Dates use the Gregorian calendar; times use UTC. ++contains exactly two digits, ++they are the last two digits of a year from 1900 through 1999; ++otherwise, ++.I Y ++contains all the digits of the year. ++Dates use the Gregorian calendar. ++Times use UTC, except that for portability's sake leap seconds are not allowed; ++implementations that support leap seconds should output ++.B 59 ++for ++.I ss ++during an inserted leap second, and should accept ++.B 59 ++for a deleted leap second. + .PP + The + .I newphrase +@@ -144,16 +156,23 @@ + field in order of decreasing numbers. + The + .B head +-field in the +-.I admin +-node points to the head of that sequence (i.e., contains ++field points to the head of that sequence (i.e., contains + the highest pair). + The + .B branch +-node in the admin node indicates the default ++field indicates the default + branch (or revision) for most \*r operations. + If empty, the default + branch is the highest branch on the trunk. ++The ++.B symbols ++field associates symbolic names with revisions. ++For example, if the file contains ++.B "symbols rr:1.1;" ++then ++.B rr ++is a name for revision ++.BR 1.1 . + .PP + All + .I delta + diff --git a/gnu/dist/cvs/doc/cvs-paper.ms b/gnu/dist/cvs/doc/cvs-paper.ms new file mode 100644 index 00000000000..ea9445ad168 --- /dev/null +++ b/gnu/dist/cvs/doc/cvs-paper.ms @@ -0,0 +1,1069 @@ +.\" soelim cvs.ms | pic | tbl | troff -ms +.\" @(#)cvs.ms 1.2 92/01/30 +.\" +.\" troff source to the cvs USENIX article, Winter 1990, Washington, D.C. +.\" Copyright (c) 1989, Brian Berliner +.\" +.\" 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 1, 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. +.\" +.\" The author can be reached at: berliner@prisma.com +.\" +.de SP +.if n .sp +.if t .sp .5 +.. +.de hl +.br +.in +0.5i +\l'\\n(LLu-1i' +.in -0.5i +.sp +.. +.OH "" +.nr PS 11 +.nr PO 1.25i +.pl -0.2i +.TL +.ps 14 +.ft B +.nf +CVS II: +Parallelizing Software Development +.fi +.ft +.ps +.AU +.ps 12 +.ft I +Brian Berliner +.ft +.ps +.AI +.ps 12 +.ft I +Prisma, Inc. +5465 Mark Dabling Blvd. +Colorado Springs, CO 80918 +berliner@prisma.com +.ft +.ps +.AB +The program described in this paper fills a need in the UNIX +community for a freely available tool to manage software revision and +release control in a multi-developer, multi-directory, multi-group +environment. +This tool also addresses the increasing need for tracking third-party vendor +source distributions while trying to maintain local modifications to +earlier releases. +.AE +.NH +Background +.PP +In large software development projects, it is usually necessary for more +than one software developer to be modifying (usually different) modules of the +code at the same time. +Some of these code modifications are done in an +experimental sense, at least until the code functions correctly, and some +testing of the entire program is usually necessary. +Then, the modifications are returned to a master source repository +so that others in the project can +enjoy the new bug-fix or functionality. +In order to manage such a project, some sort of revision control system is +necessary. +.PP +Specifically, UNIX\** +.FS +UNIX is a registered trademark of AT&T. +.FE +kernel development is an excellent example of the +problems that an adequate revision control system must address. +The SunOS\** +.FS +SunOS is a trademark of Sun Microsystems, Inc. +.FE +kernel is composed of over a thousand files spread across a +hierarchy of dozens of directories.\** +.FS +Yes, the SunOS 4.0 kernel is composed of over a \fIthousand\fP files! +.FE +Pieces of the kernel must be edited +by many software developers within an organization. +While undesirable in +theory, it is not uncommon to have two or more people making +modifications to the same file within the kernel sources in +order to facilitate a desired change. +Existing revision control systems like +.SM +RCS +.LG +[Tichy] or +.SM +SCCS +.LG +[Bell] serialize file modifications by +allowing only one developer to have a writable copy of a particular file at +any one point in time. +That developer is said to +have \*Qlocked\*U the file for his exclusive use, and no other developer is +allowed to check out a writable copy of the file until the locking +developer has finished impeding others' productivity. +Development pressures of productivity and deadlines +often force organizations to require that multiple developers be able to +simultaneously edit +copies of the same revision controlled file. +.PP +The necessity for multiple developers to modify the same file concurrently +questions the value of serialization-based policies in traditional revision +control. +This paper discusses the approach that +Prisma took in adapting a standard revision control system, +.SM +RCS\c +.LG +, along with an existing public-domain collection of shell scripts that sits +atop +.SM +RCS +.LG +and provides the basic conflict-resolution algorithms. +The resulting +program, \fBcvs\fP, addresses not only the issue of conflict-resolution in +a multi-developer open-editing environment, but also the issues of +software release control and vendor source support and integration. +.NH +The CVS Program +.PP +\fBcvs\fP +(Concurrent Versions System) +is a front end to the +.SM +RCS +.LG +revision control system which extends +the notion of revision control from a collection of files in a single +directory to a hierarchical collection of directories each containing +revision controlled files. +Directories and files in the \fBcvs\fP system can be combined together in +many ways to form a software release. +\fBcvs\fP +provides the functions necessary to manage these software releases and to +control the concurrent editing of source files among multiple software +developers. +.PP +The six major features of \fBcvs\fP are listed below, and will be +described in more detail in the following sections: +.RS +.IP 1. +Concurrent access and conflict-resolution algorithms to guarantee that +source changes are not \*Qlost.\*U +.IP 2. +Support for tracking third-party vendor source distributions while +maintaining the local modifications made to those sources. +.IP 3. +A flexible module database that provides a symbolic mapping of names to +components of a larger software distribution. +This symbolic mapping provides for location independence within the software +release and, for example, allows one to check out a copy of the \*Qdiff\*U +program without ever knowing that the sources to \*Qdiff\*U actually reside +in the \*Qbin/diff\*U directory. +.IP 4. +Configurable logging support allows all \*Qcommitted\*U source file changes +to be logged using an arbitrary program to save the log messages in a file, +notesfile, or news database. +.IP 5. +A software release can be symbolically tagged and checked out at any time +based on that tag. +An exact copy of a previous software release can be checked out at +any time, \fIregardless\fP of whether files or directories have been +added/removed from the \*Qcurrent\*U software release. +As well, +a \*Qdate\*U can be used to check out the \fIexact\fP version of the software +release as of the specified date. +.IP 6. +A \*Qpatch\*U format file [Wall] can be produced between two software +releases, even if the releases span multiple directories. +.RE +.PP +The sources maintained by \fBcvs\fP are kept within a single directory +hierarchy known as the \*Qsource repository.\*U +This \*Qsource repository\*U holds the actual +.SM +RCS +.LG +\*Q,v\*U files directly, as well as a special per-repository directory +(\c +.SM +CVSROOT.adm\c +.LG +) which contains a small number of administrative files that describe the +repository and how it can be accessed. +See Figure 1 for a picture of the \fBcvs\fP tree. +.KF +.hl +.DS B +.PS +line from 4.112,9.200 to 5.550,8.887 +line from 5.447,8.884 to 5.550,8.887 to 5.458,8.933 +line from 4.112,9.200 to 4.550,8.950 +line from 4.451,8.978 to 4.550,8.950 to 4.476,9.021 +line from 4.112,9.200 to 3.737,8.887 +line from 3.798,8.971 to 3.737,8.887 to 3.830,8.932 +line from 3.612,8.762 to 4.737,8.137 +line from 4.638,8.164 to 4.737,8.137 to 4.662,8.208 +line from 3.612,8.762 to 3.737,8.137 +line from 3.693,8.231 to 3.737,8.137 to 3.742,8.240 +line from 3.612,8.762 to 2.612,8.200 +line from 2.687,8.271 to 2.612,8.200 to 2.712,8.227 +line from 2.362,9.262 to 2.737,8.950 +line from 2.645,8.995 to 2.737,8.950 to 2.677,9.033 +line from 2.362,9.262 to 1.925,8.950 +line from 1.992,9.028 to 1.925,8.950 to 2.021,8.988 +line from 3.362,9.762 to 4.050,9.387 +line from 3.950,9.413 to 4.050,9.387 to 3.974,9.457 +line from 3.362,9.762 to 2.487,9.387 +line from 2.570,9.450 to 2.487,9.387 to 2.589,9.404 +.ps 11 +"newfs.c,v" at 4.487,8.043 ljust +.ps 11 +"mkfs.c,v" at 3.487,8.043 ljust +.ps 11 +"Makefile,v" at 2.237,8.043 ljust +.ps 11 +"newfs" at 3.487,8.793 ljust +.ps 11 +"halt.c,v" at 5.487,8.793 ljust +.ps 11 +"Makefile,v" at 4.237,8.793 ljust +.ps 11 +"modules,v" at 2.487,8.793 ljust +.ps 11 +"loginfo,v" at 1.488,8.793 ljust +.ps 11 +"etc" at 3.987,9.293 ljust +.ps 11 +"CVSROOT.adm" at 1.988,9.293 ljust +.ps 11 +"/src/master" at 2.987,9.793 ljust +.PE +.DE +.hl +.ce 100 +.LG +\fBFigure 1.\fP +.SM +\fBcvs\fP Source Repository +.ce 0 +.sp +.KE +.NH 2 +Software Conflict Resolution\** +.FS +The basic conflict-resolution algorithms +used in the \fBcvs\fP program find their roots +in the original work done by Dick Grune at Vrije Universiteit in Amsterdam +and posted to \fBcomp.sources.unix\fP in the volume 6 release sometime in 1986. +This original version of \fBcvs\fP was a collection of shell scripts that +combined to form a front end to the +.SM +RCS +.LG +programs. +.FE +.PP +\fBcvs\fP allows several software developers to edit personal copies of a +revision controlled file concurrently. +The revision number of each checked out file is maintained independently +for each user, and \fBcvs\fP forces the checked out file to be current with +the \*Qhead\*U revision before it can be \*Qcommitted\*U as a permanent change. +A checked out file is brought up-to-date with the \*Qhead\*U revision using +the \*Qupdate\*U command of \fBcvs\fP. +This command compares the \*Qhead\*U revision number with that of the user's +file and performs an +.SM +RCS +.LG +merge operation if they are not the same. +The result of the merge is a file that contains the user's modifications +and those modifications that were \*Qcommitted\*U after the user +checked out his version of the file (as well as a backup copy of the +user's original file). +\fBcvs\fP points out any conflicts during the merge. +It is the user's responsibility to resolve these conflicts +and to \*Qcommit\*U his/her changes when ready. +.PP +Although the \fBcvs\fP conflict-resolution algorithm was defined in 1986, +it is remarkably similar to the \*QCopy-Modify-Merge\*U scenario included +with NSE\** +.FS +NSE is the Network Software Environment, a product of Sun Microsystems, Inc. +.FE +and described in [Honda] and [Courington]. +The following explanation from [Honda] also applies to \fBcvs\fP: +.QP +Simply stated, a developer copies an object without locking it, modifies +the copy, and then merges the modified copy with the original. +This paradigm allows developers to work in isolation from one another since +changes are made to copies of objects. +Because locks are not used, development is not serialized and can proceed +in parallel. +Developers, however, must merge objects after the changes have been made. +In particular, a developer must resolve conflicts when the same object has +been modified by someone else. +.PP +In practice, Prisma has found that conflicts that occur when the same +object has been modified by someone else are quite rare. +When they do happen, the changes made by the other developer are usually +easily resolved. +This practical use has shown that the \*QCopy-Modify-Merge\*U paradigm is a +correct and useful one. +.NH 2 +Tracking Third-Party Source Distributions +.PP +Currently, a large amount of software is based on source +distributions from a third-party distributor. +It is often the case that local modifications are to be made to this +distribution, \fIand\fP that the vendor's future releases should be +tracked. +Rolling your local modifications forward into the new vendor release is a +time-consuming task, but \fBcvs\fP can ease this burden somewhat. +The \fBcheckin\fP program of \fBcvs\fP initially sets up a source +repository by integrating the source modules directly from the vendor's +release, preserving the directory hierarchy of the vendor's distribution. +The branch support of +.SM +RCS +.LG +is used to build this vendor release as a branch of the main +.SM +RCS +.LG +trunk. +Figure 2 shows how the \*Qhead\*U tracks a sample vendor +branch when no local modifications have been made to the file. +.KF +.hl +.DS B +.PS +ellipse at 3.237,6.763 wid 1.000 ht 0.500 +dashwid = 0.050i +line dashed from 3.237,7.513 to 3.737,7.513 to 3.737,9.762 to 4.237,9.762 +line from 4.138,9.737 to 4.237,9.762 to 4.138,9.787 +line dashed from 2.237,8.262 to 3.237,8.262 to 3.237,7.013 +line from 3.212,7.112 to 3.237,7.013 to 3.262,7.112 +line from 3.737,6.763 to 4.237,6.763 +line from 4.138,6.737 to 4.237,6.763 to 4.138,6.788 +line from 2.237,6.763 to 2.737,6.763 +line from 2.637,6.737 to 2.737,6.763 to 2.637,6.788 +line from 1.738,6.013 to 1.738,6.513 +line from 1.762,6.413 to 1.738,6.513 to 1.713,6.413 +line from 1.238,7.013 to 2.237,7.013 to 2.237,6.513 to 1.238,6.513 to 1.238,7.013 +line from 4.237,9.012 to 5.237,9.012 to 5.237,8.512 to 4.237,8.512 to 4.237,9.012 +line from 4.237,8.012 to 5.237,8.012 to 5.237,7.513 to 4.237,7.513 to 4.237,8.012 +line from 4.237,7.013 to 5.237,7.013 to 5.237,6.513 to 4.237,6.513 to 4.237,7.013 +line from 4.737,7.013 to 4.737,7.513 +line from 4.763,7.413 to 4.737,7.513 to 4.712,7.413 +line from 4.737,8.012 to 4.737,8.512 +line from 4.763,8.412 to 4.737,8.512 to 4.712,8.412 +line from 4.237,10.012 to 5.237,10.012 to 5.237,9.512 to 4.237,9.512 to 4.237,10.012 +line from 4.737,9.012 to 4.737,9.512 +line from 4.763,9.412 to 4.737,9.512 to 4.712,9.412 +line from 5.987,5.013 to 5.987,6.013 to 0.988,6.013 to 0.988,5.013 to 5.987,5.013 +.ps 11 +"\"HEAD\"" at 1.550,8.231 ljust +.ps 11 +"'SunOS'" at 2.987,6.293 ljust +.ps 11 +"1.1.1" at 3.050,6.793 ljust +.ps 11 +"1.1" at 1.613,6.793 ljust +.ps 11 +"1.1.1.1" at 4.487,6.793 ljust +.ps 11 +"1.1.1.2" at 4.487,7.793 ljust +.ps 11 +"1.1.1.3" at 4.487,8.793 ljust +.ps 11 +"1.1.1.4" at 4.487,9.793 ljust +.ps 11 +"'SunOS_4_0'" at 5.487,6.793 ljust +.ps 11 +"'SunOS_4_0_1'" at 5.487,7.793 ljust +.ps 11 +"'YAPT_5_5C'" at 5.487,8.793 ljust +.ps 11 +"'SunOS_4_0_3'" at 5.487,9.793 ljust +.ps 11 +"rcsfile.c,v" at 2.987,5.543 ljust +.PE +.DE +.hl +.ce 100 +.LG +\fBFigure 2.\fP +.SM +\fBcvs\fP Vendor Branch Example +.ce 0 +.sp .3 +.KE +Once this is done, developers can check out files and make local changes to +the vendor's source distribution. +These local changes form a new branch to the tree which is then used as the +source for future check outs. +Figure 3 shows how the \*Qhead\*U moves to the main +.SM +RCS +.LG +trunk when a local modification is made. +.KF +.hl +.DS B +.PS +ellipse at 3.237,6.763 wid 1.000 ht 0.500 +dashwid = 0.050i +line dashed from 2.800,9.075 to 1.738,9.075 to 1.738,8.012 +line from 1.713,8.112 to 1.738,8.012 to 1.762,8.112 +line from 1.738,7.013 to 1.738,7.513 +line from 1.762,7.413 to 1.738,7.513 to 1.713,7.413 +line from 1.238,8.012 to 2.237,8.012 to 2.237,7.513 to 1.238,7.513 to 1.238,8.012 +line from 3.737,6.763 to 4.237,6.763 +line from 4.138,6.737 to 4.237,6.763 to 4.138,6.788 +line from 2.237,6.763 to 2.737,6.763 +line from 2.637,6.737 to 2.737,6.763 to 2.637,6.788 +line from 1.738,6.013 to 1.738,6.513 +line from 1.762,6.413 to 1.738,6.513 to 1.713,6.413 +line from 1.238,7.013 to 2.237,7.013 to 2.237,6.513 to 1.238,6.513 to 1.238,7.013 +line from 4.237,9.012 to 5.237,9.012 to 5.237,8.512 to 4.237,8.512 to 4.237,9.012 +line from 4.237,8.012 to 5.237,8.012 to 5.237,7.513 to 4.237,7.513 to 4.237,8.012 +line from 4.237,7.013 to 5.237,7.013 to 5.237,6.513 to 4.237,6.513 to 4.237,7.013 +line from 4.737,7.013 to 4.737,7.513 +line from 4.763,7.413 to 4.737,7.513 to 4.712,7.413 +line from 4.737,8.012 to 4.737,8.512 +line from 4.763,8.412 to 4.737,8.512 to 4.712,8.412 +line from 4.237,10.012 to 5.237,10.012 to 5.237,9.512 to 4.237,9.512 to 4.237,10.012 +line from 4.737,9.012 to 4.737,9.512 +line from 4.763,9.412 to 4.737,9.512 to 4.712,9.412 +line from 5.987,5.013 to 5.987,6.013 to 0.988,6.013 to 0.988,5.013 to 5.987,5.013 +.ps 11 +"1.2" at 1.613,7.793 ljust +.ps 11 +"\"HEAD\"" at 2.862,9.043 ljust +.ps 11 +"'SunOS'" at 2.987,6.293 ljust +.ps 11 +"1.1.1" at 3.050,6.793 ljust +.ps 11 +"1.1" at 1.613,6.793 ljust +.ps 11 +"1.1.1.1" at 4.487,6.793 ljust +.ps 11 +"1.1.1.2" at 4.487,7.793 ljust +.ps 11 +"1.1.1.3" at 4.487,8.793 ljust +.ps 11 +"1.1.1.4" at 4.487,9.793 ljust +.ps 11 +"'SunOS_4_0'" at 5.487,6.793 ljust +.ps 11 +"'SunOS_4_0_1'" at 5.487,7.793 ljust +.ps 11 +"'YAPT_5_5C'" at 5.487,8.793 ljust +.ps 11 +"'SunOS_4_0_3'" at 5.487,9.793 ljust +.ps 11 +"rcsfile.c,v" at 2.987,5.543 ljust +.PE +.DE +.hl +.ce 100 +.LG +\fBFigure 3.\fP +.SM +\fBcvs\fP Local Modification to Vendor Branch +.ce 0 +.sp +.KE +.PP +When a new version of the vendor's source distribution arrives, the +\fBcheckin\fP program adds the new and changed vendor's files to the +already existing source repository. +For files that have not been changed locally, the new file from the +vendor becomes the current \*Qhead\*U revision. +For files that have been modified locally, \fBcheckin\fP warns that the +file must be merged with the new vendor release. +The \fBcvs\fP \*Qjoin\*U command is a useful tool that aids this process by +performing the necessary +.SM +RCS +.LG +merge, as is done above when performing an \*Qupdate.\*U +.PP +There is also limited support for \*Qdual\*U derivations for source files. +See Figure 4 for a sample dual-derived file. +.KF +.hl +.DS B +.PS +ellipse at 2.337,8.575 wid 0.700 ht 0.375 +ellipse at 2.312,9.137 wid 0.700 ht 0.375 +line from 1.225,9.012 to 1.225,9.363 +line from 1.250,9.263 to 1.225,9.363 to 1.200,9.263 +line from 0.875,9.725 to 1.600,9.725 to 1.600,9.363 to 0.875,9.363 to 0.875,9.725 +line from 0.875,9.012 to 1.600,9.012 to 1.600,8.650 to 0.875,8.650 to 0.875,9.012 +line from 4.050,10.200 to 4.775,10.200 to 4.775,9.850 to 4.050,9.850 to 4.050,10.200 +line from 4.050,9.475 to 4.775,9.475 to 4.775,9.113 to 4.050,9.113 to 4.050,9.475 +line from 4.050,8.762 to 4.775,8.762 to 4.775,8.400 to 4.050,8.400 to 4.050,8.762 +line from 4.425,8.762 to 4.425,9.113 +line from 4.450,9.013 to 4.425,9.113 to 4.400,9.013 +line from 4.425,9.475 to 4.425,9.850 +line from 4.450,9.750 to 4.425,9.850 to 4.400,9.750 +line from 3.050,10.000 to 3.775,10.000 to 3.775,9.637 to 3.050,9.637 to 3.050,10.000 +line from 3.050,9.312 to 3.775,9.312 to 3.775,8.950 to 3.050,8.950 to 3.050,9.312 +line from 0.713,7.325 to 0.713,8.075 to 4.925,8.075 to 4.925,7.325 to 0.713,7.325 +line from 1.238,8.075 to 1.238,8.637 +line from 1.262,8.537 to 1.238,8.637 to 1.213,8.537 +line from 1.613,8.825 to 1.975,8.575 +line from 1.878,8.611 to 1.975,8.575 to 1.907,8.652 +line from 2.675,8.575 to 4.050,8.575 +line from 3.950,8.550 to 4.050,8.575 to 3.950,8.600 +line from 2.675,9.137 to 3.050,9.137 +line from 2.950,9.112 to 3.050,9.137 to 2.950,9.162 +line from 3.425,9.325 to 3.425,9.637 +line from 3.450,9.537 to 3.425,9.637 to 3.400,9.537 +line from 1.613,8.825 to 1.925,9.137 +line from 1.872,9.049 to 1.925,9.137 to 1.837,9.084 +.ps 11 +"'BSD'" at 2.138,9.481 ljust +.ps 11 +"1.2" at 1.113,9.543 ljust +.ps 11 +"1.1" at 1.125,8.831 ljust +.ps 11 +"1.1.1.1" at 4.175,8.543 ljust +.ps 11 +"1.1.1.2" at 4.175,9.281 ljust +.ps 11 +"1.1.1.3" at 4.175,9.993 ljust +.ps 11 +"1.1.2.2" at 3.175,9.793 ljust +.ps 11 +"1.1.2.1" at 3.175,9.106 ljust +.ps 11 +"rcsfile.c,v" at 2.425,7.706 ljust +.ps 11 +"1.1.1" at 2.175,8.568 ljust +.ps 11 +"'SunOS'" at 2.125,8.243 ljust +.ps 11 +"1.1.2" at 2.163,9.131 ljust +.PE +.DE +.hl +.ce 100 +.LG +\fBFigure 4.\fP +.SM +\fBcvs\fP Support For \*QDual\*U Derivations +.ce 0 +.sp +.KE +This example tracks the SunOS distribution but includes major changes from +Berkeley. +These BSD files are saved directly in the +.SM +RCS +.LG +file off a new branch. +.NH 2 +Location Independent Module Database +.PP +\fBcvs\fP contains support for a simple, yet powerful, \*Qmodule\*U database. +For reasons of efficiency, this database is stored in \fBndbm\fP\|(3) format. +The module database is used to apply names to collections of directories +and files as a matter of convenience for checking out pieces of a large +software distribution. +The database records the physical location of the sources as a form of +information hiding, allowing one to check out whole directory hierarchies +or individual files without regard for their actual location within the +global source distribution. +.PP +Consider the following small sample of a module database, which must be +tailored manually to each specific source repository environment: +.DS +\f(CW #key [-option argument] directory [files...] + diff bin/diff + libc lib/libc + sys -o sys/tools/make_links sys + modules -i mkmodules CVSROOT.adm modules + kernel -a sys lang/adb + ps bin Makefile ps.c\fP +.DE +.PP +The \*Qdiff\*U and \*Qlibc\*U modules refer to whole directory hierarchies that +are extracted on check out. +The \*Qsys\*U module extracts the \*Qsys\*U hierarchy, and runs the +\*Qmake_links\*U program at the end of the check out process (the \fI-o\fP +option specifies a program to run on check\fIo\fPut). +The \*Qmodules\*U module allows one to edit the module database file and +runs the \*Qmkmodules\*U program on check\fIi\fPn to regenerate the +\fBndbm\fP database that \fBcvs\fP uses. +The \*Qkernel\*U module is an alias (as the \fI-a\fP option specifies) +which causes the remaining arguments after the \fI-a\fP to be interpreted +exactly as if they had been specified on the command line. +This is useful for objects that require shared pieces of code from far away +places to be compiled (as is the case with the kernel debugger, \fBkadb\fP, +which shares code with the standard \fBadb\fP debugger). +The \*Qps\*U module shows that the source for \*Qps\*U lives in the \*Qbin\*U +directory, but only \fIMakefile\fP and \fIps.c\fP are required to build the +object. +.PP +The module database at Prisma is now populated for the entire UNIX +distribution and thereby allows us to issue the +following convenient commands to check out components of the UNIX +distribution without regard for their actual location within the master source +repository: +.DS +\f(CW example% cvs checkout diff + example% cvs checkout libc ps + example% cd diff; make\fP +.DE +.PP +In building the module database file, it is quite possible to have name +conflicts within a global software distribution. +For example, SunOS provides two \fBcat\fP programs: +one for the standard environment, \fI/bin/cat\fP, and one for the System V +environment, \fI/usr/5bin/cat\fP. +We resolved this conflict by naming the standard \fBcat\fP module +\*Qcat\*U, and the System V \fBcat\fP module \*Q5cat\*U. +Similar name modifications must be applied to other conflicting names, as +might be found between a utility program and a library function, though +Prisma chose not to include individual library functions within the module +database at this time. +.NH 2 +Configurable Logging Support +.PP +The \fBcvs\fP \*Qcommit\*U command is used to make a permanent change to the +master source repository (where the +.SM +RCS +.LG +\*Q,v\*U files live). +Whenever a \*Qcommit\*U is done, the log message for the change is carefully +logged by an arbitrary program (in a file, notesfile, news database, or +mail). +For example, a collection of these updates can be used to produce release +notices. +\fBcvs\fP can be configured to send log updates through one or more filter +programs, based on a regular expression match on the directory that is +being changed. +This allows multiple related or unrelated projects to exist within a single +\fBcvs\fP source repository tree, with each different project sending its +\*Qcommit\*U reports to a unique log device. +.PP +A sample logging configuration file might look as follows: +.DS +\f(CW #regex filter-program + DEFAULT /usr/local/bin/nfpipe -t %s utils.updates + ^diag /usr/local/bin/nfpipe -t %s diag.updates + ^local /usr/local/bin/nfpipe -t %s local.updates + ^perf /usr/local/bin/nfpipe -t %s perf.updates + ^sys /usr/local/bin/nfpipe -t %s kernel.updates\fP +.DE +.PP +This sample allows the diagnostics and performance groups to +share the same source repository with the kernel and utilities groups. +Changes that they make are sent directly to their own notesfile [Essick] +through the \*Qnfpipe\*U program. +A sufficiently simple title is substituted for the \*Q%s\*U argument before +the filter program is executed. +This logging configuration file is tailored manually to each specific +source repository environment. +.NH 2 +Tagged Releases and Dates +.PP +Any release can be given a symbolic tag name that is stored directly in the +.SM +RCS +.LG +files. +This tag can be used at any time to get an exact copy of any previous +release. +With equal ease, one can also extract an exact copy of the source files as +of any arbitrary date in the past as well. +Thus, all that's required to tag the current kernel, and to tag the kernel +as of the Fourth of July is: +.DS +\f(CW example% cvs tag TEST_KERNEL kernel + example% cvs tag -D 'July 4' PATRIOTIC_KERNEL kernel\fP +.DE +The following command would retrieve an exact copy of the test kernel at +some later date: +.DS +\f(CW example% cvs checkout -fp -rTEST_KERNEL kernel\fP +.DE +The \fI-f\fP option causes only files that match the specified tag to be +extracted, while the \fI-p\fP option automatically prunes empty directories. +Consequently, directories added to the kernel after the test kernel was +tagged are not included in the newly extracted copy of the test kernel. +.PP +The \fBcvs\fP date support has exactly the same interface as that provided +with +.SM +RCS\c +.LG +, however \fBcvs\fP must process the \*Q,v\*U files directly due to the +special handling required by the vendor branch support. +The standard +.SM +RCS +.LG +date handling only processes one branch (or the main trunk) when checking +out based on a date specification. +\fBcvs\fP must instead process the current \*Qhead\*U branch and, if a +match is not found, proceed to look for a match on the vendor branch. +This, combined with reasons of performance, is why \fBcvs\fP processes +revision (symbolic and numeric) and date specifications directly from the +\*Q,v\*U files. +.NH 2 +Building \*Qpatch\*U Source Distributions +.PP +\fBcvs\fP can produce a \*Qpatch\*U format [Wall] output file which can be +used to bring a previously released software distribution current with the +newest release. +This patch file supports an entire directory hierarchy within a single +patch, as well as being able to add whole new files to the previous +release. +One can combine symbolic revisions and dates together to display changes in +a very generic way: +.DS +\f(CW example% cvs patch -D 'December 1, 1988' \e + -D 'January 1, 1989' sys\fP +.DE +This example displays the kernel changes made in the month of December, +1988. +To release a patch file, for example, to take the \fBcvs\fP distribution +from version 1.0 to version 1.4 might be done as follows: +.DS +\f(CW example% cvs patch -rCVS_1_0 -rCVS_1_4 cvs\fP +.DE +.NH +CVS Experience +.NH 2 +Statistics +.PP +A quick summary of the scale that \fBcvs\fP is addressing today +can be found in Table 1. +.KF +.TS +box center tab(:); +c s +c s +c | c +l | n . +\fB\s+2Revision Control Statistics at Prisma +as of 11/11/89\fP\s-2 +_ +How Many...:Total += +Files:17243 +Directories:1005 +Lines of code:3927255 +Removed files:131 +Software developers:14 +Software groups:6 +Megabytes of source:128 +.TE +.ce 100 +.LG +\fBTable 1.\fP +.SM +\fBcvs\fP Statistics +.ce 0 +.sp .3 +.KE +Table 2 shows the history of files changed or added and the number +of source lines affected by the change at Prisma. +Only changes made to the kernel sources are included. +.KF +.TS +box center tab(:); +c s s s s +c s s s s +c || c | c || c | c +c || c | c || c | c +l || n | n || n | n. +\fB\s+2Prisma Kernel Source File Changes +By Month, 1988-1989\fP\s-2 +_ +Month:# Changed:# Lines:# Added:# Lines +\^:Files:Changed:Files:Added += +Dec:87:3619:68:9266 +Jan:39:4324:0:0 +Feb:73:1578:5:3550 +Mar:99:5301:18:11461 +Apr:112:7333:11:5759 +May:138:5371:17:13986 +Jun:65:2261:27:12875 +Jul:34:2000:1:58 +Aug:65:6378:8:4724 +Sep:266:23410:113:39965 +Oct:22:621:1:155 +Total:1000:62196:269:101799 +.TE +.ce 100 +.LG +\fBTable 2.\fP +.SM +\fBcvs\fP Usage History for the Kernel +.ce 0 +.sp +.KE +The large number of source file changes made in September are the result of +merging the SunOS 4.0.3 sources into the kernel. +This merge process is described in section 3.3. +.NH 2 +Performance +.PP +The performance of \fBcvs\fP is currently quite reasonable. +Little effort has been expended on tuning \fBcvs\fP, although performance +related decisions were made during the \fBcvs\fP design. +For example, \fBcvs\fP parses the +.SM +RCS +.LG +\*Q,v\*U files directly instead of running an +.SM +RCS +.LG +process. +This includes following branches as well as integrating with the vendor +source branches and the main trunk when checking out files based on a date. +.PP +Checking out the entire kernel source tree (1223 files/59 directories) +currently takes 16 wall clock minutes on a Sun-4/280. +However, bringing the tree up-to-date with the current kernel sources, once +it has been checked out, takes only 1.5 wall clock minutes. +Updating the \fIcomplete\fP 128 MByte source tree under \fBcvs\fP control +(17243 files/1005 directories) takes roughly 28 wall clock minutes and +utilizes one-third of the machine. +For now this is entirely acceptable; improvements on these numbers will +possibly be made in the future. +.NH 2 +The SunOS 4.0.3 Merge +.PP +The true test of the \fBcvs\fP vendor branch support came with the arrival +of the SunOS 4.0.3 source upgrade tape. +As described above, the \fBcheckin\fP program was used to install the new +sources and the resulting output file listed the files that had been +locally modified, needing to be merged manually. +For the kernel, there were 94 files in conflict. +The \fBcvs\fP \*Qjoin\*U command was used on each of the 94 conflicting +files, and the remaining conflicts were resolved. +.PP +The \*Qjoin\*U command performs an \fBrcsmerge\fP operation. +This in turn uses \fI/usr/lib/diff3\fP to produce a three-way diff file. +As it happens, the \fBdiff3\fP program has a hard-coded limit of 200 +source-file changes maximum. +This proved to be too small for a few of the kernel files that needed +merging by hand, due to the large number of local changes that Prisma had +made. +The \fBdiff3\fP problem was solved by increasing the hard-coded limit by an +order of magnitude. +.PP +The SunOS 4.0.3 kernel source upgrade distribution contained +346 files, 233 of which were modifications to previously released files, +and 113 of which were newly added files. +\fBcheckin\fP added the 113 new files to the source repository +without intervention. +Of the 233 modified files, 139 dropped in cleanly by \fBcheckin\fP, since +Prisma had not made any local changes to them, and 94 required manual +merging due to local modifications. +The 233 modified files consisted of 20,766 lines of differences. +It took one developer two days to manually merge the 94 files using the +\*Qjoin\*U command and resolving conflicts manually. +An additional day was required for kernel debugging. +The entire process of merging over 20,000 lines of differences was +completed in less than a week. +This one time-savings alone was justification enough for the \fBcvs\fP +development effort; we expect to gain even more when tracking future SunOS +releases. +.NH +Future Enhancements and Current Bugs +.PP +Since \fBcvs\fP was designed to be incomplete, for reasons of design +simplicity, there are naturally a good +number of enhancements that can be made to make it more useful. +As well, some nuisances exist in the current implementation. +.RS +.IP \(bu 3 +\fBcvs\fP does not currently \*Qremember\*U who has a checked out a copy of a +module. +As a result, it is impossible to know who might be working on the same +module that you are. +A simple-minded database that is updated nightly would likely suffice. +.IP \(bu 3 +Signal processing, keyboard interrupt handling in particular, is currently +somewhat weak. +This is due to the heavy use of the \fBsystem\fP\|(3) library +function to execute +.SM +RCS +.LG +programs like \fBco\fP and \fBci\fP. +It sometimes takes multiple interrupts to make \fBcvs\fP quit. +This can be fixed by using a home-grown \fBsystem\fP\|() replacement. +.IP \(bu 3 +Security of the source repository is currently not dealt with directly. +The usual UNIX approach of user-group-other security permissions through +the file system is utilized, but nothing else. +\fBcvs\fP could likely be a set-group-id executable that checks a +protected database to verify user access permissions for particular objects +before allowing any operations to affect those objects. +.IP \(bu 3 +With every checked-out directory, \fBcvs\fP maintains some administrative +files that record the current revision numbers of the checked-out files as +well as the location of the respective source repository. +\fBcvs\fP does not recover nicely at all if these administrative files are +removed. +.IP \(bu 3 +The source code for \fBcvs\fP has been tested extensively on Sun-3 and +Sun-4 systems, all running SunOS 4.0 or later versions of the operating +system. +Since the code has not yet been compiled under other platforms, the overall +portability of the code is still questionable. +.IP \(bu 3 +As witnessed in the previous section, the \fBcvs\fP method for tracking +third party vendor source distributions can work quite nicely. +However, if the vendor changes the directory structure or the file names +within the source distribution, \fBcvs\fP has no way of matching the old +release with the new one. +It is currently unclear as to how to solve this, though it is certain to +happen in practice. +.RE +.NH +Availability +.PP +The \fBcvs\fP program sources can be found in a recent posting to the +\fBcomp.sources.unix\fP newsgroup. +It is also currently available via anonymous ftp from \*Qprisma.com\*U. +Copying rights for \fBcvs\fP will be covered by the GNU General Public +License. +.NH +Summary +.PP +Prisma has used \fBcvs\fP since December, 1988. +It has evolved to meet our specific needs of revision and release control. +We will make our code freely available so that others can +benefit from our work, and can enhance \fBcvs\fP to meet broader needs yet. +.PP +Many of the other software release and revision control systems, like the +one described in [Glew], appear to use a collection of tools that are +geared toward specific environments \(em one set of tools for the kernel, +one set for \*Qgeneric\*U software, one set for utilities, and one set for +kernel and utilities. +Each of these tool sets apparently handle some specific aspect of the +problem uniquely. +\fBcvs\fP took a somewhat different approach. +File sharing through symbolic or hard links is not addressed; instead, the +disk space is simply burned since it is \*Qcheap.\*U +Support for producing objects for multiple architectures is not addressed; +instead, a parallel checked-out source tree must be used for each +architecture, again wasting disk space to simplify complexity and ease of +use \(em punting on this issue allowed \fIMakefile\fPs to remain +unchanged, unlike the approach taken in [Mahler], thereby maintaining closer +compatibility with the third-party vendor sources. +\fBcvs\fP is essentially a source-file server, making no assumptions or +special handling of the sources that it controls. +To \fBcvs\fP: +.QP +A source is a source, of course, of course, unless of course the source is +Mr. Ed.\** +.FS +\fBcvs\fP, of course, does not really discriminate against Mr. Ed.\** +.FE +.FS +Yet. +.FE +.LP +Sources are maintained, saved, and retrievable at any time based on +symbolic or numeric revision or date in the past. +It is entirely up to \fBcvs\fP wrapper programs to provide for release +environments and such. +.PP +The major advantage of \fBcvs\fP over the +many other similar systems that have already been designed is the +simplicity of \fBcvs\fP. +\fBcvs\fP contains only three programs that do all the work of release +and revision control, and two manually-maintained administrative +files for each source repository. +Of course, the deciding factor of any tool is whether people use it, and if +they even \fIlike\fP to use it. +At Prisma, \fBcvs\fP prevented members of the kernel +group from killing each other. +.NH +Acknowledgements +.PP +Many thanks to Dick Grune at Vrije Universiteit in Amsterdam for his work +on the original version of \fBcvs\fP and for making it available to the +world. +Thanks to Jeff Polk of Prisma for helping with the design of the module +database, vendor branch support, and for writing the \fBcheckin\fP shell +script. +Thanks also to the entire software group at Prisma for taking the +time to review the paper and correct my grammar. +.NH +References +.IP [Bell] 12 +Bell Telephone Laboratories. +\*QSource Code Control System User's Guide.\*U +\fIUNIX System III Programmer's Manual\fP, October 1981. +.IP [Courington] 12 +Courington, W. +\fIThe Network Software Environment\fP, +Sun Technical Report FE197-0, Sun Microsystems Inc, February 1989. +.IP [Essick] 12 +Essick, Raymond B. and Robert Bruce Kolstad. +\fINotesfile Reference Manual\fP, +Department of Computer Science Technical Report #1081, +University of Illinois at Urbana-Champaign, Urbana, Illinois, +1982, p. 26. +.IP [Glew] 12 +Glew, Andy. +\*QBoxes, Links, and Parallel Trees: +Elements of a Configuration Management System.\*U +\fIWorkshop Proceedings of the Software Management Conference\fP, USENIX, +New Orleans, April 1989. +.IP [Grune] 12 +Grune, Dick. +Distributed the original shell script version of \fBcvs\fP in the +\fBcomp.sources.unix\fP volume 6 release in 1986. +.IP [Honda] 12 +Honda, Masahiro and Terrence Miller. +\*QSoftware Management Using a CASE Environment.\*U +\fIWorkshop Proceedings of the Software Management Conference\fP, USENIX, +New Orleans, April 1989. +.IP [Mahler] 12 +Mahler, Alex and Andreas Lampen. +\*QAn Integrated Toolset for Engineering Software Configurations.\*U +\fIProceedings of the ACM SIGSOFT/SIGPLAN Software Engineering Symposium on +Practical Software Development Environments\fP, ACM, Boston, November 1988. +Described is the \fBshape\fP toolkit posted to the +\fBcomp.sources.unix\fP newsgroup in the volume 19 release. +.IP [Tichy] 12 +Tichy, Walter F. +\*QDesign, Implementation, and Evaluation of a Revision Control System.\*U +\fIProceedings of the 6th International Conference on Software +Engineering\fP, IEEE, Tokyo, September 1982. +.IP [Wall] 12 +Wall, Larry. +The \fBpatch\fP program is an indispensable tool for applying a diff file +to an original. +Can be found on uunet.uu.net in ~ftp/pub/patch.tar. diff --git a/gnu/dist/cvs/doc/cvs.texinfo b/gnu/dist/cvs/doc/cvs.texinfo new file mode 100644 index 00000000000..3768fe91742 --- /dev/null +++ b/gnu/dist/cvs/doc/cvs.texinfo @@ -0,0 +1,13561 @@ +\input texinfo @c -*-texinfo-*- +@comment Documentation for CVS. +@comment Copyright (C) 1992, 1993, 1999 Signum Support AB +@comment Copyright (C) 1993 Free Software Foundation, Inc. + +@comment This file is part of the CVS distribution. + +@comment CVS is free software; you can redistribute it and/or modify +@comment it under the terms of the GNU General Public License as published by +@comment the Free Software Foundation; either version 2, or (at your option) +@comment any later version. + +@comment CVS is distributed in the hope that it will be useful, +@comment but WITHOUT ANY WARRANTY; without even the implied warranty of +@comment MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +@comment GNU General Public License for more details. + +@c See ../README for A4 vs. US letter size. +@c When we provided A4 postscript, and people tried to +@c print it on US letter, the usual complaint was that the +@c page numbers would get cut off. +@c If one prints US letter on A4, reportedly there is +@c some extra space at the top and/or bottom, and the side +@c margins are a bit narrow, but no text is lost. +@c +@c See +@c http://www.ft.uni-erlangen.de/~mskuhn/iso-paper.html +@c for more on paper sizes. Insuring that margins are +@c big enough to print on either A4 or US letter does +@c indeed seem to be the usual approach (RFC2346). + +@c This document seems to get overfull hboxes with some +@c frequency (probably because the tendency is to +@c sanity-check it with "make info" and run TeX less +@c often). The big ugly boxes just seem to add insult +@c to injury, and I'm not aware of them helping to fix +@c the overfull hboxes at all. +@finalout + +@setfilename cvs.info +@include CVSvn.texi +@settitle CVS---Concurrent Versions System +@setchapternewpage odd + +@c -- TODO list: +@c -- Fix all lines that match "^@c -- " +@c -- Also places marked with FIXME should be manual +@c problems (as opposed to FIXCVS for CVS problems). + +@ifinfo +@format +START-INFO-DIR-ENTRY +* CVS: (cvs). Concurrent Versions System +END-INFO-DIR-ENTRY +@end format +@end ifinfo + +@ifinfo +Copyright @copyright{} 1992, 1993 Signum Support AB +Copyright @copyright{} 1993, 1994 Free Software Foundation, Inc. + +Permission is granted to make and distribute verbatim copies of +this manual provided the copyright notice and this permission notice +are preserved on all copies. + +@ignore +Permission is granted to process this file through Tex and print the +results, provided the printed document carries copying permission +notice identical to this one except for the removal of this paragraph +(this paragraph not being relevant to the printed manual). + +@end ignore +Permission is granted to copy and distribute modified versions of this +manual under the conditions for verbatim copying, provided also that the +entire resulting derived work is distributed under the terms of a +permission notice identical to this one. + +Permission is granted to copy and distribute translations of this manual +into another language, under the above conditions for modified versions, +except that this permission notice may be stated in a translation +approved by the Free Software Foundation. +@end ifinfo + +@comment The titlepage section does not appear in the Info file. +@titlepage +@sp 4 +@comment The title is printed in a large font. +@center @titlefont{Version Management} +@sp +@center @titlefont{with} +@sp +@center @titlefont{CVS} +@sp 2 +@center for @sc{cvs} @value{CVSVN} +@comment -release- +@sp 3 +@center Per Cederqvist et al + +@comment The following two commands start the copyright page +@comment for the printed manual. This will not appear in the Info file. +@page +@vskip 0pt plus 1filll +Copyright @copyright{} 1992, 1993 Signum Support AB + +Permission is granted to make and distribute verbatim copies of +this manual provided the copyright notice and this permission notice +are preserved on all copies. + +Permission is granted to copy and distribute modified versions of this +manual under the conditions for verbatim copying, provided also that the +entire resulting derived work is distributed under the terms of a +permission notice identical to this one. + +Permission is granted to copy and distribute translations of this manual +into another language, under the above conditions for modified versions, +except that this permission notice may be stated in a translation +approved by the Free Software Foundation. +@end titlepage + +@comment ================================================================ +@comment The real text starts here +@comment ================================================================ + +@ifinfo +@c --------------------------------------------------------------------- +@node Top +@top +@c Note: there is a space after that @top command. +@c The texinfo-format-buffer Emacs function and +@c the makeinfo shell command disagree on what arguments +@c @top takes; @top followed by a single space is +@c something they can both cope with. + +This info manual describes how to use and administer +@sc{cvs} version @value{CVSVN}. +@end ifinfo + +@c This menu is pretty long. Not sure how easily that +@c can be fixed (no brilliant ideas right away)... +@menu +* Overview:: An introduction to CVS +* Repository:: Where all your sources are stored +* Starting a new project:: Starting a project with CVS +* Revisions:: Numeric and symbolic names for revisions +* Branching and merging:: Diverging/rejoining branches of development +* Recursive behavior:: CVS descends directories +* Adding and removing:: Adding/removing/renaming files/directories +* History browsing:: Viewing the history of files in various ways + +CVS and the Real World. +----------------------- +* Binary files:: CVS can handle binary files +* Multiple developers:: How CVS helps a group of developers +* Revision management:: Policy questions for revision management +* Keyword substitution:: CVS can include the revision inside the file +* Tracking sources:: Tracking third-party sources +* Builds:: Issues related to CVS and builds +* Special Files:: Devices, links and other non-regular files + +References. +----------- +* CVS commands:: CVS commands share some things +* Invoking CVS:: Quick reference to CVS commands +* Administrative files:: Reference manual for the Administrative files +* Environment variables:: All environment variables which affect CVS +* Compatibility:: Upgrading CVS versions +* Troubleshooting:: Some tips when nothing works +* Credits:: Some of the contributors to this manual +* BUGS:: Dealing with bugs in CVS or this manual +* Index:: Index +@end menu + +@c --------------------------------------------------------------------- +@node Overview +@chapter Overview +@cindex Overview + +This chapter is for people who have never used +@sc{cvs}, and perhaps have never used version control +software before. + +If you are already familiar with @sc{cvs} and are just +trying to learn a particular feature or remember a +certain command, you can probably skip everything here. + +@menu +* What is CVS?:: What you can do with @sc{cvs} +* What is CVS not?:: Problems @sc{cvs} doesn't try to solve +* A sample session:: A tour of basic @sc{cvs} usage +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node What is CVS? +@section What is CVS? +@cindex What is CVS? +@cindex Introduction to CVS +@cindex CVS, introduction to + +@sc{cvs} is a version control system. Using it, you can +record the history of your source files. + +@c -- /// +@c -- ///Those who cannot remember the past are condemned to repeat it. +@c -- /// -- George Santayana +@c -- ////// + +@c -- Insert history quote here! +For example, bugs sometimes creep in when +software is modified, and you might not detect the bug +until a long time after you make the modification. +With @sc{cvs}, you can easily retrieve old versions to see +exactly which change caused the bug. This can +sometimes be a big help. + +You could of course save every version of every file +you have ever created. This would +however waste an enormous amount of disk space. @sc{cvs} +stores all the versions of a file in a single file in a +clever way that only stores the differences between +versions. + +@sc{cvs} also helps you if you are part of a group of people working +on the same project. It is all too easy to overwrite +each others' changes unless you are extremely careful. +Some editors, like @sc{gnu} Emacs, try to make sure that +the same file is never modified by two people at the +same time. Unfortunately, if someone is using another +editor, that safeguard will not work. @sc{cvs} solves this problem +by insulating the different developers from each other. Every +developer works in his own directory, and @sc{cvs} merges +the work when each developer is done. + +@cindex History of CVS +@cindex CVS, history of +@cindex Credits (CVS program) +@cindex Contributors (CVS program) +@sc{cvs} started out as a bunch of shell scripts written by +Dick Grune, posted to the newsgroup +@code{comp.sources.unix} in the volume 6 +release of December, 1986. While no actual code from +these shell scripts is present in the current version +of @sc{cvs} much of the @sc{cvs} conflict resolution algorithms +come from them. + +In April, 1989, Brian Berliner designed and coded @sc{cvs}. +Jeff Polk later helped Brian with the design of the @sc{cvs} +module and vendor branch support. + +@cindex Source, getting CVS source +You can get @sc{cvs} in a variety of ways, including +free download from the internet. For more information +on downloading @sc{cvs} and other @sc{cvs} topics, see: + +@example +http://www.cyclic.com/ +http://www.loria.fr/~molli/cvs-index.html +@end example + +@cindex Mailing list +@cindex List, mailing list +@cindex Newsgroups +There is a mailing list, known as @w{@code{info-cvs}}, +devoted to @sc{cvs}. To subscribe or +unsubscribe +write to +@w{@code{info-cvs-request@@gnu.org}}. +If you prefer a usenet group, the right +group is @code{comp.software.config-mgmt} which is for +@sc{cvs} discussions (along with other configuration +management systems). In the future, it might be +possible to create a +@code{comp.software.config-mgmt.cvs}, but probably only +if there is sufficient @sc{cvs} traffic on +@code{comp.software.config-mgmt}. +@c Other random data is that past attempts to create a +@c gnu.* group have failed (the relevant authorities +@c say they'll do it, but don't), and that tale was very +@c skeptical of comp.software.config-mgmt.cvs when the +@c subject came up around 1995 or so (for one +@c thing, because creating it would be a "reorg" which +@c would need to take a more comprehensive look at the +@c whole comp.software.config-mgmt.* hierarchy). + +You can also subscribe to the bug-cvs mailing list, +described in more detail in @ref{BUGS}. To subscribe +send mail to bug-cvs-request@@gnu.org. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node What is CVS not? +@section What is CVS not? +@cindex What is CVS not? + +@sc{cvs} can do a lot of things for you, but it does +not try to be everything for everyone. + +@table @asis +@item @sc{cvs} is not a build system. + +Though the structure of your repository and modules +file interact with your build system +(e.g. @file{Makefile}s), they are essentially +independent. + +@sc{cvs} does not dictate how you build anything. It +merely stores files for retrieval in a tree structure +you devise. + +@sc{cvs} does not dictate how to use disk space in the +checked out working directories. If you write your +@file{Makefile}s or scripts in every directory so they +have to know the relative positions of everything else, +you wind up requiring the entire repository to be +checked out. + +If you modularize your work, and construct a build +system that will share files (via links, mounts, +@code{VPATH} in @file{Makefile}s, etc.), you can +arrange your disk usage however you like. + +But you have to remember that @emph{any} such system is +a lot of work to construct and maintain. @sc{cvs} does +not address the issues involved. + +Of course, you should place the tools created to +support such a build system (scripts, @file{Makefile}s, +etc) under @sc{cvs}. + +Figuring out what files need to be rebuilt when +something changes is, again, something to be handled +outside the scope of @sc{cvs}. One traditional +approach is to use @code{make} for building, and use +some automated tool for generating the dependencies which +@code{make} uses. + +See @ref{Builds}, for more information on doing builds +in conjunction with @sc{cvs}. + +@item @sc{cvs} is not a substitute for management. + +Your managers and project leaders are expected to talk +to you frequently enough to make certain you are aware +of schedules, merge points, branch names and release +dates. If they don't, @sc{cvs} can't help. + +@sc{cvs} is an instrument for making sources dance to +your tune. But you are the piper and the composer. No +instrument plays itself or writes its own music. + +@item @sc{cvs} is not a substitute for developer communication. + +When faced with conflicts within a single file, most +developers manage to resolve them without too much +effort. But a more general definition of ``conflict'' +includes problems too difficult to solve without +communication between developers. + +@sc{cvs} cannot determine when simultaneous changes +within a single file, or across a whole collection of +files, will logically conflict with one another. Its +concept of a @dfn{conflict} is purely textual, arising +when two changes to the same base file are near enough +to spook the merge (i.e. @code{diff3}) command. + +@sc{cvs} does not claim to help at all in figuring out +non-textual or distributed conflicts in program logic. + +For example: Say you change the arguments to function +@code{X} defined in file @file{A}. At the same time, +someone edits file @file{B}, adding new calls to +function @code{X} using the old arguments. You are +outside the realm of @sc{cvs}'s competence. + +Acquire the habit of reading specs and talking to your +peers. + + +@item @sc{cvs} does not have change control + +Change control refers to a number of things. First of +all it can mean @dfn{bug-tracking}, that is being able +to keep a database of reported bugs and the status of +each one (is it fixed? in what release? has the bug +submitter agreed that it is fixed?). For interfacing +@sc{cvs} to an external bug-tracking system, see the +@file{rcsinfo} and @file{verifymsg} files +(@pxref{Administrative files}). + +Another aspect of change control is keeping track of +the fact that changes to several files were in fact +changed together as one logical change. If you check +in several files in a single @code{cvs commit} +operation, @sc{cvs} then forgets that those files were +checked in together, and the fact that they have the +same log message is the only thing tying them +together. Keeping a @sc{gnu} style @file{ChangeLog} +can help somewhat. +@c FIXME: should have an xref to a section which talks +@c more about keeping ChangeLog's with CVS, but that +@c section hasn't been written yet. + +Another aspect of change control, in some systems, is +the ability to keep track of the status of each +change. Some changes have been written by a developer, +others have been reviewed by a second developer, and so +on. Generally, the way to do this with @sc{cvs} is to +generate a diff (using @code{cvs diff} or @code{diff}) +and email it to someone who can then apply it using the +@code{patch} utility. This is very flexible, but +depends on mechanisms outside @sc{cvs} to make sure +nothing falls through the cracks. + +@item @sc{cvs} is not an automated testing program + +It should be possible to enforce mandatory use of a +testsuite using the @code{commitinfo} file. I haven't +heard a lot about projects trying to do that or whether +there are subtle gotchas, however. + +@item @sc{cvs} does not have a builtin process model + +Some systems provide ways to ensure that changes or +releases go through various steps, with various +approvals as needed. Generally, one can accomplish +this with @sc{cvs} but it might be a little more work. +In some cases you'll want to use the @file{commitinfo}, +@file{loginfo}, @file{rcsinfo}, or @file{verifymsg} +files, to require that certain steps be performed +before cvs will allow a checkin. Also consider whether +features such as branches and tags can be used to +perform tasks such as doing work in a development tree +and then merging certain changes over to a stable tree +only once they have been proven. +@end table + +@c --------------------------------------------------------------------- +@node A sample session +@section A sample session +@cindex Example of a work-session +@cindex Getting started +@cindex Work-session, example of +@cindex tc, Trivial Compiler (example) +@cindex Trivial Compiler (example) + +@c I think an example is a pretty good way to start. But +@c somewhere in here, maybe after the sample session, +@c we need something which is kind of +@c a "roadmap" which is more directed at sketching out +@c the functionality of CVS and pointing people to +@c various other parts of the manual. As it stands now +@c people who read in order get dumped right into all +@c manner of hair regarding remote repositories, +@c creating a repository, etc. +@c +@c The following was in the old Basic concepts node. I don't +@c know how good a job it does at introducing modules, +@c or whether they need to be introduced so soon, but +@c something of this sort might go into some +@c introductory material somewhere. +@ignore +@cindex Modules (intro) +The repository contains directories and files, in an +arbitrary tree. The @dfn{modules} feature can be used +to group together a set of directories or files into a +single entity (@pxref{modules}). A typical usage is to +define one module per project. +@end ignore + +As a way of introducing @sc{cvs}, we'll go through a +typical work-session using @sc{cvs}. The first thing +to understand is that @sc{cvs} stores all files in a +centralized @dfn{repository} (@pxref{Repository}); this +section assumes that a repository is set up. +@c I'm not sure that the sentence concerning the +@c repository quite tells the user what they need to +@c know at this point. Might need to expand on "centralized" +@c slightly (maybe not here, maybe further down in the example?) + +Suppose you are working on a simple compiler. The source +consists of a handful of C files and a @file{Makefile}. +The compiler is called @samp{tc} (Trivial Compiler), +and the repository is set up so that there is a module +called @samp{tc}. + +@menu +* Getting the source:: Creating a workspace +* Committing your changes:: Making your work available to others +* Cleaning up:: Cleaning up +* Viewing differences:: Viewing differences +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Getting the source +@subsection Getting the source +@cindex Getting the source +@cindex Checking out source +@cindex Fetching source +@cindex Source, getting from CVS +@cindex Checkout, example + +The first thing you must do is to get your own working copy of the +source for @samp{tc}. For this, you use the @code{checkout} command: + +@example +$ cvs checkout tc +@end example + +@noindent +This will create a new directory called @file{tc} and populate it with +the source files. + +@example +$ cd tc +$ ls +CVS Makefile backend.c driver.c frontend.c parser.c +@end example + +The @file{CVS} directory is used internally by +@sc{cvs}. Normally, you should not modify or remove +any of the files in it. + +You start your favorite editor, hack away at @file{backend.c}, and a couple +of hours later you have added an optimization pass to the compiler. +A note to @sc{rcs} and @sc{sccs} users: There is no need to lock the files that +you want to edit. @xref{Multiple developers}, for an explanation. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Committing your changes +@subsection Committing your changes +@cindex Committing changes +@cindex Log message entry +@cindex CVSEDITOR, environment variable +@cindex EDITOR, environment variable + +When you have checked that the compiler is still compilable you decide +to make a new version of @file{backend.c}. This will +store your new @file{backend.c} in the repository and +make it available to anyone else who is using that same +repository. + +@example +$ cvs commit backend.c +@end example + +@noindent +@sc{cvs} starts an editor, to allow you to enter a log +message. You type in ``Added an optimization pass.'', +save the temporary file, and exit the editor. + +The environment variable @code{$CVSEDITOR} determines +which editor is started. If @code{$CVSEDITOR} is not +set, then if the environment variable @code{$EDITOR} is +set, it will be used. If both @code{$CVSEDITOR} and +@code{$EDITOR} are not set then there is a default +which will vary with your operating system, for example +@code{vi} for unix or @code{notepad} for Windows +NT/95. + +@cindex VISUAL, environment variable +In addition, @sc{cvs} checks the @code{$VISUAL} environment +variable. Opinions vary on whether this behavior is desirable and +whether future releases of @sc{cvs} should check @code{$VISUAL} or +ignore it. You will be OK either way if you make sure that +@code{$VISUAL} is either unset or set to the same thing as +@code{$EDITOR}. + +@c This probably should go into some new node +@c containing detailed info on the editor, rather than +@c the intro. In fact, perhaps some of the stuff with +@c CVSEDITOR and -m and so on should too. +When @sc{cvs} starts the editor, it includes a list of +files which are modified. For the @sc{cvs} client, +this list is based on comparing the modification time +of the file against the modification time that the file +had when it was last gotten or updated. Therefore, if +a file's modification time has changed but its contents +have not, it will show up as modified. The simplest +way to handle this is simply not to worry about it---if +you proceed with the commit @sc{cvs} will detect that +the contents are not modified and treat it as an +unmodified file. The next @code{update} will clue +@sc{cvs} in to the fact that the file is unmodified, +and it will reset its stored timestamp so that the file +will not show up in future editor sessions. +@c FIXCVS: Might be nice if "commit" and other commands +@c would reset that timestamp too, but currently commit +@c doesn't. +@c FIXME: Need to talk more about the process of +@c prompting for the log message. Like show an example +@c of what it pops up in the editor, for example. Also +@c a discussion of how to get the "a)bort, c)ontinue, +@c e)dit" prompt and what to do with it. Might also +@c work in the suggestion that if you want a diff, you +@c should make it before running commit (someone +@c suggested that the diff pop up in the editor. I'm +@c not sure that is better than telling people to run +@c "cvs diff" first if that is what they want, but if +@c we want to tell people that, the manual possibly +@c should say it). + +If you want to avoid +starting an editor you can specify the log message on +the command line using the @samp{-m} flag instead, like +this: + +@example +$ cvs commit -m "Added an optimization pass" backend.c +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Cleaning up +@subsection Cleaning up +@cindex Cleaning up +@cindex Working copy, removing +@cindex Removing your working copy +@cindex Releasing your working copy + +Before you turn to other tasks you decide to remove your working copy of +tc. One acceptable way to do that is of course + +@example +$ cd .. +$ rm -r tc +@end example + +@noindent +but a better way is to use the @code{release} command (@pxref{release}): + +@example +$ cd .. +$ cvs release -d tc +M driver.c +? tc +You have [1] altered files in this repository. +Are you sure you want to release (and delete) directory `tc': n +** `release' aborted by user choice. +@end example + +The @code{release} command checks that all your modifications have been +committed. If history logging is enabled it also makes a note in the +history file. @xref{history file}. + +When you use the @samp{-d} flag with @code{release}, it +also removes your working copy. + +In the example above, the @code{release} command wrote a couple of lines +of output. @samp{? tc} means that the file @file{tc} is unknown to @sc{cvs}. +That is nothing to worry about: @file{tc} is the executable compiler, +and it should not be stored in the repository. @xref{cvsignore}, +for information about how to make that warning go away. +@xref{release output}, for a complete explanation of +all possible output from @code{release}. + +@samp{M driver.c} is more serious. It means that the +file @file{driver.c} has been modified since it was +checked out. + +The @code{release} command always finishes by telling +you how many modified files you have in your working +copy of the sources, and then asks you for confirmation +before deleting any files or making any note in the +history file. + +You decide to play it safe and answer @kbd{n @key{RET}} +when @code{release} asks for confirmation. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Viewing differences +@subsection Viewing differences +@cindex Viewing differences +@cindex Diff + +You do not remember modifying @file{driver.c}, so you want to see what +has happened to that file. + +@example +$ cd tc +$ cvs diff driver.c +@end example + +This command runs @code{diff} to compare the version of @file{driver.c} +that you checked out with your working copy. When you see the output +you remember that you added a command line option that enabled the +optimization pass. You check it in, and release the module. +@c FIXME: we haven't yet defined the term "check in". + +@example +$ cvs commit -m "Added an optimization pass" driver.c +Checking in driver.c; +/usr/local/cvsroot/tc/driver.c,v <-- driver.c +new revision: 1.2; previous revision: 1.1 +done +$ cd .. +$ cvs release -d tc +? tc +You have [0] altered files in this repository. +Are you sure you want to release (and delete) directory `tc': y +@end example + +@c --------------------------------------------------------------------- +@node Repository +@chapter The Repository +@cindex Repository (intro) +@cindex Repository, example +@cindex Layout of repository +@cindex Typical repository +@cindex /usr/local/cvsroot, as example repository +@cindex cvsroot + +The @sc{cvs} @dfn{repository} stores a complete copy of +all the files and directories which are under version +control. + +Normally, you never access any of the files in the +repository directly. Instead, you use @sc{cvs} +commands to get your own copy of the files into a +@dfn{working directory}, and then +work on that copy. When you've finished a set of +changes, you check (or @dfn{commit}) them back into the +repository. The repository then contains the changes +which you have made, as well as recording exactly what +you changed, when you changed it, and other such +information. Note that the repository is not a +subdirectory of the working directory, or vice versa; +they should be in separate locations. +@c Need some example, e.g. repository +@c /usr/local/cvsroot; working directory +@c /home/joe/sources. But this node is too long +@c as it is; need a little reorganization... + +@cindex :local:, setting up +@sc{cvs} can access a repository by a variety of +means. It might be on the local computer, or it might +be on a computer across the room or across the world. +To distinguish various ways to access a repository, the +repository name can start with an @dfn{access method}. +For example, the access method @code{:local:} means to +access a repository directory, so the repository +@code{:local:/usr/local/cvsroot} means that the +repository is in @file{/usr/local/cvsroot} on the +computer running @sc{cvs}. For information on other +access methods, see @ref{Remote repositories}. + +@c Can se say this more concisely? Like by passing +@c more of the buck to the Remote repositories node? +If the access method is omitted, then if the repository +does not contain @samp{:}, then @code{:local:} is +assumed. If it does contain @samp{:} then either +@code{:ext:} or @code{:server:} is assumed. For +example, if you have a local repository in +@file{/usr/local/cvsroot}, you can use +@code{/usr/local/cvsroot} instead of +@code{:local:/usr/local/cvsroot}. But if (under +Windows NT, for example) your local repository is +@file{c:\src\cvsroot}, then you must specify the access +method, as in @code{:local:c:\src\cvsroot}. + +@c This might appear to go in Repository storage, but +@c actually it is describing something which is quite +@c user-visible, when you do a "cvs co CVSROOT". This +@c isn't necessary the perfect place for that, though. +The repository is split in two parts. @file{$CVSROOT/CVSROOT} contains +administrative files for @sc{cvs}. The other directories contain the actual +user-defined modules. + +@menu +* Specifying a repository:: Telling CVS where your repository is +* Repository storage:: The structure of the repository +* Working directory storage:: The structure of working directories +* Intro administrative files:: Defining modules +* Multiple repositories:: Multiple repositories +* Creating a repository:: Creating a repository +* Backing up:: Backing up a repository +* Moving a repository:: Moving a repository +* Remote repositories:: Accessing repositories on remote machines +* Read-only access:: Granting read-only access to the repository +* Server temporary directory:: The server creates temporary directories +@end menu + +@node Specifying a repository +@section Telling CVS where your repository is + +There are several ways to tell @sc{cvs} +where to find the repository. You can name the +repository on the command line explicitly, with the +@code{-d} (for "directory") option: + +@example +cvs -d /usr/local/cvsroot checkout yoyodyne/tc +@end example + +@cindex .profile, setting CVSROOT in +@cindex .cshrc, setting CVSROOT in +@cindex .tcshrc, setting CVSROOT in +@cindex .bashrc, setting CVSROOT in +@cindex CVSROOT, environment variable + Or you can set the @code{$CVSROOT} environment +variable to an absolute path to the root of the +repository, @file{/usr/local/cvsroot} in this example. +To set @code{$CVSROOT}, @code{csh} and @code{tcsh} +users should have this line in their @file{.cshrc} or +@file{.tcshrc} files: + +@example +setenv CVSROOT /usr/local/cvsroot +@end example + +@noindent +@code{sh} and @code{bash} users should instead have these lines in their +@file{.profile} or @file{.bashrc}: + +@example +CVSROOT=/usr/local/cvsroot +export CVSROOT +@end example + +@cindex Root file, in CVS directory +@cindex CVS/Root file + A repository specified with @code{-d} will +override the @code{$CVSROOT} environment variable. +Once you've checked a working copy out from the +repository, it will remember where its repository is +(the information is recorded in the +@file{CVS/Root} file in the working copy). + +The @code{-d} option and the @file{CVS/Root} file both +override the @code{$CVSROOT} environment variable. If +@code{-d} option differs from @file{CVS/Root}, the +former is used. Of course, for proper operation they +should be two ways of referring to the same repository. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Repository storage +@section How data is stored in the repository +@cindex Repository, how data is stored + +For most purposes it isn't important @emph{how} +@sc{cvs} stores information in the repository. In +fact, the format has changed in the past, and is likely +to change in the future. Since in almost all cases one +accesses the repository via @sc{cvs} commands, such +changes need not be disruptive. + +However, in some cases it may be necessary to +understand how @sc{cvs} stores data in the repository, +for example you might need to track down @sc{cvs} locks +(@pxref{Concurrency}) or you might need to deal with +the file permissions appropriate for the repository. + +@menu +* Repository files:: What files are stored in the repository +* File permissions:: File permissions +* Windows permissions:: Issues specific to Windows +* Attic:: Some files are stored in the Attic +* CVS in repository:: Additional information in CVS directory +* Locks:: CVS locks control concurrent accesses +* CVSROOT storage:: A few things about CVSROOT are different +@end menu + +@node Repository files +@subsection Where files are stored within the repository + +@c @cindex Filenames, legal +@c @cindex Legal filenames +@c Somewhere we need to say something about legitimate +@c characters in filenames in working directory and +@c repository. Not "/" (not even on non-unix). And +@c here is a specific set of issues: +@c Files starting with a - are handled inconsistently. They can not +@c be added to a repository with an add command, because it they are +@c interpreted as a switch. They can appear in a repository if they are +@c part of a tree that is imported. They can not be removed from the tree +@c once they are there. +@c Note that "--" *is* supported (as a +@c consequence of using GNU getopt). Should document +@c this somewhere ("Common options"?). The other usual technique, +@c "./-foo", isn't as effective, at least for "cvs add" +@c which doesn't support pathnames containing "/". + +The overall structure of the repository is a directory +tree corresponding to the directories in the working +directory. For example, supposing the repository is in + +@example +/usr/local/cvsroot +@end example + +@noindent +here is a possible directory tree (showing only the +directories): + +@example +@t{/usr} + | + +--@t{local} + | | + | +--@t{cvsroot} + | | | + | | +--@t{CVSROOT} + | (administrative files) + | + +--@t{gnu} + | | + | +--@t{diff} + | | (source code to @sc{gnu} diff) + | | + | +--@t{rcs} + | | (source code to @sc{rcs}) + | | + | +--@t{cvs} + | (source code to @sc{cvs}) + | + +--@t{yoyodyne} + | + +--@t{tc} + | | + | +--@t{man} + | | + | +--@t{testing} + | + +--(other Yoyodyne software) +@end example + +With the directories are @dfn{history files} for each file +under version control. The name of the history file is +the name of the corresponding file with @samp{,v} +appended to the end. Here is what the repository for +the @file{yoyodyne/tc} directory might look like: +@c FIXME: Should also mention CVS (CVSREP) +@c FIXME? Should we introduce Attic with an xref to +@c Attic? Not sure whether that is a good idea or not. +@example + @code{$CVSROOT} + | + +--@t{yoyodyne} + | | + | +--@t{tc} + | | | + +--@t{Makefile,v} + +--@t{backend.c,v} + +--@t{driver.c,v} + +--@t{frontend.c,v} + +--@t{parser.c,v} + +--@t{man} + | | + | +--@t{tc.1,v} + | + +--@t{testing} + | + +--@t{testpgm.t,v} + +--@t{test2.t,v} +@end example + +@cindex History files +@cindex RCS history files +@c The first sentence, about what history files +@c contain, is kind of redundant with our intro to what the +@c repository does in node Repository.... +The history files contain, among other things, enough +information to recreate any revision of the file, a log +of all commit messages and the user-name of the person +who committed the revision. The history files are +known as @dfn{RCS files}, because the first program to +store files in that format was a version control system +known as @sc{rcs}. For a full +description of the file format, see the @code{man} page +@cite{rcsfile(5)}, distributed with @sc{rcs}, or the +file @file{doc/RCSFILES} in the @sc{cvs} source +distribution. This +file format has become very common---many systems other +than @sc{cvs} or @sc{rcs} can at least import history +files in this format. +@c FIXME: Think about including documentation for this +@c rather than citing it? In the long run, getting +@c this to be a standard (not sure if we can cope with +@c a standards process as formal as IEEE/ANSI/ISO/etc, +@c though...) is the way to go, so maybe citing is +@c better. + +The @sc{rcs} files used in @sc{cvs} differ in a few +ways from the standard format. The biggest difference +is magic branches; for more information see @ref{Magic +branch numbers}. Also in @sc{cvs} the valid tag names +are a subset of what @sc{rcs} accepts; for @sc{cvs}'s +rules see @ref{Tags}. + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node File permissions +@subsection File permissions +@c -- Move this to @node Creating a repository or similar +@cindex Security, file permissions in repository +@cindex File permissions, general +@cindex Permissions, general +@c FIXME: we need to somehow reflect "permissions in +@c repository" versus "permissions in working +@c directory" in the index entries. +@cindex Group +@cindex Read-only files, in repository +All @samp{,v} files are created read-only, and you +should not change the permission of those files. The +directories inside the repository should be writable by +the persons that have permission to modify the files in +each directory. This normally means that you must +create a UNIX group (see group(5)) consisting of the +persons that are to edit the files in a project, and +set up the repository so that it is that group that +owns the directory. +@c See also comment in commitinfo node regarding cases +@c which are really awkward with unix groups. + +This means that you can only control access to files on +a per-directory basis. + +Note that users must also have write access to check +out files, because @sc{cvs} needs to create lock files +(@pxref{Concurrency}). + +@c CVS seems to use CVSUMASK in picking permissions for +@c val-tags, but maybe we should say more about this. +@c Like val-tags gets created by someone who doesn't +@c have CVSUMASK set right? +Also note that users must have write access to the +@file{CVSROOT/val-tags} file. @sc{cvs} uses it to keep +track of what tags are valid tag names (it is sometimes +updated when tags are used, as well as when they are +created). + +Each @sc{rcs} file will be owned by the user who last +checked it in. This has little significance; what +really matters is who owns the directories. + +@cindex CVSUMASK, environment variable +@cindex Umask, for repository files +@sc{cvs} tries to set up reasonable file permissions +for new directories that are added inside the tree, but +you must fix the permissions manually when a new +directory should have different permissions than its +parent directory. If you set the @code{CVSUMASK} +environment variable that will control the file +permissions which @sc{cvs} uses in creating directories +and/or files in the repository. @code{CVSUMASK} does +not affect the file permissions in the working +directory; such files have the permissions which are +typical for newly created files, except that sometimes +@sc{cvs} creates them read-only (see the sections on +watches, @ref{Setting a watch}; -r, @ref{Global +options}; or @code{CVSREAD}, @ref{Environment variables}). +@c FIXME: Need more discussion of which +@c group should own the file in the repository. +@c Include a somewhat detailed example of the usual +@c case where CVSUMASK is 007, the developers are all +@c in a group, and that group owns stuff in the +@c repository. Need to talk about group ownership of +@c newly-created directories/files (on some unices, +@c such as SunOS4, setting the setgid bit on the +@c directories will make files inherit the directory's +@c group. On other unices, your mileage may vary. I +@c can't remember what POSIX says about this, if +@c anything). + +Note that using the client/server @sc{cvs} +(@pxref{Remote repositories}), there is no good way to +set @code{CVSUMASK}; the setting on the client machine +has no effect. If you are connecting with @code{rsh}, you +can set @code{CVSUMASK} in @file{.bashrc} or @file{.cshrc}, as +described in the documentation for your operating +system. This behavior might change in future versions +of @sc{cvs}; do not rely on the setting of +@code{CVSUMASK} on the client having no effect. +@c FIXME: need to explain what a umask is or cite +@c someplace which does. +@c +@c There is also a larger (largely separate) issue +@c about the meaning of CVSUMASK in a non-unix context. +@c For example, whether there is +@c an equivalent which fits better into other +@c protection schemes like POSIX.6, VMS, &c. +@c +@c FIXME: Need one place which discusses this +@c read-only files thing. Why would one use -r or +@c CVSREAD? Why would one use watches? How do they +@c interact? +@c +@c FIXME: We need to state +@c whether using CVSUMASK removes the need for manually +@c fixing permissions (in fact, if we are going to mention +@c manually fixing permission, we better document a lot +@c better just what we mean by "fix"). + +Using pserver, you will generally need stricter +permissions on the @sc{cvsroot} directory and +directories above it in the tree; see @ref{Password +authentication security}. + +@cindex Setuid +@cindex Setgid +@cindex Security, setuid +@cindex Installed images (VMS) +Some operating systems have features which allow a +particular program to run with the ability to perform +operations which the caller of the program could not. +For example, the set user ID (setuid) or set group ID +(setgid) features of unix or the installed image +feature of VMS. CVS was not written to use such +features and therefore attempting to install CVS in +this fashion will provide protection against only +accidental lapses; anyone who is trying to circumvent +the measure will be able to do so, and depending on how +you have set it up may gain access to more than just +CVS. You may wish to instead consider pserver. It +shares some of the same attributes, in terms of +possibly providing a false sense of security or opening +security holes wider than the ones you are trying to +fix, so read the documentation on pserver security +carefully if you are considering this option +(@ref{Password authentication security}). + +@node Windows permissions +@subsection File Permission issues specific to Windows +@cindex Windows, and permissions +@cindex File permissions, Windows-specific +@cindex Permissions, Windows-specific + +Some file permission issues are specific to Windows +operating systems (Windows 95, Windows NT, and +presumably future operating systems in this family. +Some of the following might apply to OS/2 but I'm not +sure). + +If you are using local CVS and the repository is on a +networked file system which is served by the Samba SMB +server, some people have reported problems with +permissions. Enabling WRITE=YES in the samba +configuration is said to fix/workaround it. +Disclaimer: I haven't investigated enough to know the +implications of enabling that option, nor do I know +whether there is something which CVS could be doing +differently in order to avoid the problem. If you find +something out, please let us know as described in +@ref{BUGS}. + +@node Attic +@subsection The attic +@cindex Attic + +You will notice that sometimes @sc{cvs} stores an +@sc{rcs} file in the @code{Attic}. For example, if the +@sc{cvsroot} is @file{/usr/local/cvsroot} and we are +talking about the file @file{backend.c} in the +directory @file{yoyodyne/tc}, then the file normally +would be in + +@example +/usr/local/cvsroot/yoyodyne/tc/backend.c,v +@end example + +but if it goes in the attic, it would be in + +@example +/usr/local/cvsroot/yoyodyne/tc/Attic/backend.c,v +@end example + +@cindex Dead state +instead. It should not matter from a user point of +view whether a file is in the attic; @sc{cvs} keeps +track of this and looks in the attic when it needs to. +But in case you want to know, the rule is that the RCS +file is stored in the attic if and only if the head +revision on the trunk has state @code{dead}. A +@code{dead} state means that file has been removed, or +never added, for that revision. For example, if you +add a file on a branch, it will have a trunk revision +in @code{dead} state, and a branch revision in a +non-@code{dead} state. +@c Probably should have some more concrete examples +@c here, or somewhere (not sure exactly how we should +@c arrange the discussion of the dead state, versus +@c discussion of the attic). + +@node CVS in repository +@subsection The CVS directory in the repository +@cindex CVS directory, in repository + +The @file{CVS} directory in each repository directory +contains information such as file attributes (in a file +called @file{CVS/fileattr}. In the +future additional files may be added to this directory, +so implementations should silently ignore additional +files. + +This behavior is implemented only by @sc{cvs} 1.7 and +later; for details see @ref{Watches Compatibility}. + +The format of the fileattr file is a series of entries +of the following form (where @samp{@{} and @samp{@}} +means the text between the braces can be repeated zero +or more times): + +@var{ent-type} @var{filename} <tab> @var{attrname} = @var{attrval} + @{; @var{attrname} = @var{attrval}@} <linefeed> + +@var{ent-type} is @samp{F} for a file, in which case the entry specifies the +attributes for that file. + +@var{ent-type} is @samp{D}, +and @var{filename} empty, to specify default attributes +to be used for newly added files. + +Other @var{ent-type} are reserved for future expansion. CVS 1.9 and older +will delete them any time it writes file attributes. +CVS 1.10 and later will preserve them. + +Note that the order of the lines is not significant; +a program writing the fileattr file may +rearrange them at its convenience. + +There is currently no way of quoting tabs or linefeeds in the +filename, @samp{=} in @var{attrname}, +@samp{;} in @var{attrval}, etc. Note: some implementations also +don't handle a NUL character in any of the fields, but +implementations are encouraged to allow it. + +By convention, @var{attrname} starting with @samp{_} is for an attribute given +special meaning by CVS; other @var{attrname}s are for user-defined attributes +(or will be, once implementations start supporting user-defined attributes). + +Builtin attributes: + +@table @code +@item _watched +Present means the file is watched and should be checked out +read-only. + +@item _watchers +Users with watches for this file. Value is +@var{watcher} > @var{type} @{ , @var{watcher} > @var{type} @} +where @var{watcher} is a username, and @var{type} +is zero or more of edit,unedit,commit separated by +@samp{+} (that is, nothing if none; there is no "none" or "all" keyword). + +@item _editors +Users editing this file. Value is +@var{editor} > @var{val} @{ , @var{editor} > @var{val} @} +where @var{editor} is a username, and @var{val} is +@var{time}+@var{hostname}+@var{pathname}, where +@var{time} is when the @code{cvs edit} command (or +equivalent) happened, +and @var{hostname} and @var{pathname} are for the working directory. +@end table + +Example: + +@c FIXME: sanity.sh should contain a similar test case +@c so we can compare this example from something from +@c Real Life(TM). See cvsclient.texi (under Notify) for more +@c discussion of the date format of _editors. +@example +Ffile1 _watched=;_watchers=joe>edit,mary>commit +Ffile2 _watched=;_editors=sue>8 Jan 1975+workstn1+/home/sue/cvs +D _watched= +@end example + +means that the file @file{file1} should be checked out +read-only. Furthermore, joe is watching for edits and +mary is watching for commits. The file @file{file2} +should be checked out read-only; sue started editing it +on 8 Jan 1975 in the directory @file{/home/sue/cvs} on +the machine @code{workstn1}. Future files which are +added should be checked out read-only. To represent +this example here, we have shown a space after +@samp{D}, @samp{Ffile1}, and @samp{Ffile2}, but in fact +there must be a single tab character there and no spaces. + +@node Locks +@subsection CVS locks in the repository + +@cindex #cvs.rfl, technical details +@cindex #cvs.wfl, technical details +@cindex #cvs.lock, technical details +@cindex Locks, cvs, technical details +For an introduction to CVS locks focusing on +user-visible behavior, see @ref{Concurrency}. The +following section is aimed at people who are writing +tools which want to access a CVS repository without +interfering with other tools acessing the same +repository. If you find yourself confused by concepts +described here, like @dfn{read lock}, @dfn{write lock}, +and @dfn{deadlock}, you might consult the literature on +operating systems or databases. + +@cindex #cvs.tfl +Any file in the repository with a name starting +with @file{#cvs.rfl.} is a read lock. Any file in +the repository with a name starting with +@file{#cvs.wfl} is a write lock. Old versions of CVS +(before CVS 1.5) also created files with names starting +with @file{#cvs.tfl}, but they are not discussed here. +The directory @file{#cvs.lock} serves as a master +lock. That is, one must obtain this lock first before +creating any of the other locks. + +To obtain a readlock, first create the @file{#cvs.lock} +directory. This operation must be atomic (which should +be true for creating a directory under most operating +systems). If it fails because the directory already +existed, wait for a while and try again. After +obtaining the @file{#cvs.lock} lock, create a file +whose name is @file{#cvs.rfl.} followed by information +of your choice (for example, hostname and process +identification number). Then remove the +@file{#cvs.lock} directory to release the master lock. +Then proceed with reading the repository. When you are +done, remove the @file{#cvs.rfl} file to release the +read lock. + +To obtain a writelock, first create the +@file{#cvs.lock} directory, as with a readlock. Then +check that there are no files whose names start with +@file{#cvs.rfl.}. If there are, remove +@file{#cvs.lock}, wait for a while, and try again. If +there are no readers, then create a file whose name is +@file{#cvs.wfl} followed by information of your choice +(for example, hostname and process identification +number). Hang on to the @file{#cvs.lock} lock. Proceed +with writing the repository. When you are done, first +remove the @file{#cvs.wfl} file and then the +@file{#cvs.lock} directory. Note that unlike the +@file{#cvs.rfl} file, the @file{#cvs.wfl} file is just +informational; it has no effect on the locking operation +beyond what is provided by holding on to the +@file{#cvs.lock} lock itself. + +Note that each lock (writelock or readlock) only locks +a single directory in the repository, including +@file{Attic} and @file{CVS} but not including +subdirectories which represent other directories under +version control. To lock an entire tree, you need to +lock each directory (note that if you fail to obtain +any lock you need, you must release the whole tree +before waiting and trying again, to avoid deadlocks). + +Note also that @sc{cvs} expects writelocks to control +access to individual @file{foo,v} files. @sc{rcs} has +a scheme where the @file{,foo,} file serves as a lock, +but @sc{cvs} does not implement it and so taking out a +@sc{cvs} writelock is recommended. See the comments at +rcs_internal_lockfile in the @sc{cvs} source code for +further discussion/rationale. + +@node CVSROOT storage +@subsection How files are stored in the CVSROOT directory +@cindex CVSROOT, storage of files + +The @file{$CVSROOT/CVSROOT} directory contains the +various administrative files. In some ways this +directory is just like any other directory in the +repository; it contains @sc{rcs} files whose names end +in @samp{,v}, and many of the @sc{cvs} commands operate +on it the same way. However, there are a few +differences. + +For each administrative file, in addition to the +@sc{rcs} file, there is also a checked out copy of the +file. For example, there is an @sc{rcs} file +@file{loginfo,v} and a file @file{loginfo} which +contains the latest revision contained in +@file{loginfo,v}. When you check in an administrative +file, @sc{cvs} should print + +@example +cvs commit: Rebuilding administrative file database +@end example + +@noindent +and update the checked out copy in +@file{$CVSROOT/CVSROOT}. If it does not, there is +something wrong (@pxref{BUGS}). To add your own files +to the files to be updated in this fashion, you can add +them to the @file{checkoutlist} administrative file +(@pxref{checkoutlist}). + +@cindex modules.db +@cindex modules.pag +@cindex modules.dir +By default, the @file{modules} file behaves as +described above. If the modules file is very large, +storing it as a flat text file may make looking up +modules slow (I'm not sure whether this is as much of a +concern now as when @sc{cvs} first evolved this +feature; I haven't seen benchmarks). Therefore, by +making appropriate edits to the @sc{cvs} source code +one can store the modules file in a database which +implements the @code{ndbm} interface, such as Berkeley +db or GDBM. If this option is in use, then the modules +database will be stored in the files @file{modules.db}, +@file{modules.pag}, and/or @file{modules.dir}. +@c I think fileattr also will use the database stuff. +@c Anything else? + +For information on the meaning of the various +administrative files, see @ref{Administrative files}. + +@node Working directory storage +@section How data is stored in the working directory + +@c FIXME: Somewhere we should discuss timestamps (test +@c case "stamps" in sanity.sh). But not here. Maybe +@c in some kind of "working directory" chapter which +@c would encompass the "Builds" one? But I'm not sure +@c whether that is a good organization (is it based on +@c what the user wants to do?). + +@cindex CVS directory, in working directory +While we are discussing @sc{cvs} internals which may +become visible from time to time, we might as well talk +about what @sc{cvs} puts in the @file{CVS} directories +in the working directories. As with the repository, +@sc{cvs} handles this information and one can usually +access it via @sc{cvs} commands. But in some cases it +may be useful to look at it, and other programs, such +as the @code{jCVS} graphical user interface or the +@code{VC} package for emacs, may need to look at it. +Such programs should follow the recommendations in this +section if they hope to be able to work with other +programs which use those files, including future +versions of the programs just mentioned and the +command-line @sc{cvs} client. + +The @file{CVS} directory contains several files. +Programs which are reading this directory should +silently ignore files which are in the directory but +which are not documented here, to allow for future +expansion. + +The files are stored according to the text file +convention for the system in question. This means that +working directories are not portable between systems +with differing conventions for storing text files. +This is intentional, on the theory that the files being +managed by CVS probably will not be portable between +such systems either. + +@table @file +@item Root +This file contains the current @sc{cvs} root, as +described in @ref{Specifying a repository}. + +@cindex Repository file, in CVS directory +@cindex CVS/Repository file +@item Repository +This file contains the directory within the repository +which the current directory corresponds with. It can +be either an absolute pathname or a relative pathname; +@sc{cvs} has had the ability to read either format +since at least version 1.3 or so. The relative +pathname is relative to the root, and is the more +sensible approach, but the absolute pathname is quite +common and implementations should accept either. For +example, after the command + +@example +cvs -d :local:/usr/local/cvsroot checkout yoyodyne/tc +@end example + +@file{Root} will contain + +@example +:local:/usr/local/cvsroot +@end example + +and @file{Repository} will contain either + +@example +/usr/local/cvsroot/yoyodyne/tc +@end example + +@noindent +or + +@example +yoyodyne/tc +@end example + +If the particular working directory does not correspond +to a directory in the repository, then @file{Repository} +should contain @file{CVSROOT/Emptydir}. + +@cindex Entries file, in CVS directory +@cindex CVS/Entries file +@item Entries +This file lists the files and directories in the +working directory. +The first character of each line indicates what sort of +line it is. If the character is unrecognized, programs +reading the file should silently skip that line, to +allow for future expansion. + +If the first character is @samp{/}, then the format is: + +@example +/@var{name}/@var{revision}/@var{timestamp}[+@var{conflict}]/@var{options}/@var{tagdate} +@end example + +where @samp{[} and @samp{]} are not part of the entry, +but instead indicate that the @samp{+} and conflict +marker are optional. @var{name} is the name of the +file within the directory. @var{revision} is the +revision that the file in the working derives from, or +@samp{0} for an added file, or @samp{-} followed by a +revision for a removed file. @var{timestamp} is the +timestamp of the file at the time that @sc{cvs} created +it; if the timestamp differs with the actual +modification time of the file it means the file has +been modified. It is stored in +the format used by the ISO C asctime() function (for +example, @samp{Sun Apr 7 01:29:26 1996}). One may +write a string which is not in that format, for +example, @samp{Result of merge}, to indicate that the +file should always be considered to be modified. This +is not a special case; to see whether a file is +modified a program should take the timestamp of the file +and simply do a string compare with @var{timestamp}. +If there was a conflict, @var{conflict} can be set to +the modification time of the file after the file has been +written with conflict markers (@pxref{Conflicts example}). +Thus if @var{conflict} is subsequently the same as the actual +modification time of the file it means that the user +has obviously not resolved the conflict. @var{options} +contains sticky options (for example @samp{-kb} for a +binary file). @var{tagdate} contains @samp{T} followed +by a tag name, or @samp{D} for a date, followed by a +sticky tag or date. Note that if @var{timestamp} +contains a pair of timestamps separated by a space, +rather than a single timestamp, you are dealing with a +version of @sc{cvs} earlier than @sc{cvs} 1.5 (not +documented here). + +The timezone on the timestamp in CVS/Entries (local or +universal) should be the same as the operating system +stores for the timestamp of the file itself. For +example, on Unix the file's timestamp is in universal +time (UT), so the timestamp in CVS/Entries should be +too. On @sc{vms}, the file's timestamp is in local +time, so @sc{cvs} on @sc{vms} should use local time. +This rule is so that files do not appear to be modified +merely because the timezone changed (for example, to or +from summer time). +@c See comments and calls to gmtime() and friends in +@c src/vers_ts.c (function time_stamp). + +If the first character of a line in @file{Entries} is +@samp{D}, then it indicates a subdirectory. @samp{D} +on a line all by itself indicates that the program +which wrote the @file{Entries} file does record +subdirectories (therefore, if there is such a line and +no other lines beginning with @samp{D}, one knows there +are no subdirectories). Otherwise, the line looks +like: + +@example +D/@var{name}/@var{filler1}/@var{filler2}/@var{filler3}/@var{filler4} +@end example + +where @var{name} is the name of the subdirectory, and +all the @var{filler} fields should be silently ignored, +for future expansion. Programs which modify +@code{Entries} files should preserve these fields. + +The lines in the @file{Entries} file can be in any order. + +@cindex Entries.Log file, in CVS directory +@cindex CVS/Entries.Log file +@item Entries.Log +This file does not record any information beyond that +in @file{Entries}, but it does provide a way to update +the information without having to rewrite the entire +@file{Entries} file, including the ability to preserve +the information even if the program writing +@file{Entries} and @file{Entries.Log} abruptly aborts. +Programs which are reading the @file{Entries} file +should also check for @file{Entries.Log}. If the latter +exists, they should read @file{Entries} and then apply +the changes mentioned in @file{Entries.Log}. After +applying the changes, the recommended practice is to +rewrite @file{Entries} and then delete @file{Entries.Log}. +The format of a line in @file{Entries.Log} is a single +character command followed by a space followed by a +line in the format specified for a line in +@file{Entries}. The single character command is +@samp{A} to indicate that the entry is being added, +@samp{R} to indicate that the entry is being removed, +or any other character to indicate that the entire line +in @file{Entries.Log} should be silently ignored (for +future expansion). If the second character of the line +in @file{Entries.Log} is not a space, then it was +written by an older version of @sc{cvs} (not documented +here). + +Programs which are writing rather than reading can +safely ignore @file{Entries.Log} if they so choose. + +@cindex Entries.Backup file, in CVS directory +@cindex CVS/Entries.Backup file +@item Entries.Backup +This is a temporary file. Recommended usage is to +write a new entries file to @file{Entries.Backup}, and +then to rename it (atomically, where possible) to @file{Entries}. + +@cindex Entries.Static file, in CVS directory +@cindex CVS/Entries.Static file +@item Entries.Static +The only relevant thing about this file is whether it +exists or not. If it exists, then it means that only +part of a directory was gotten and @sc{cvs} will +not create additional files in that directory. To +clear it, use the @code{update} command with the +@samp{-d} option, which will get the additional files +and remove @file{Entries.Static}. +@c FIXME: This needs to be better documented, in places +@c other than Working Directory Storage. +@c FIXCVS: The fact that this setting exists needs to +@c be more visible to the user. For example "cvs +@c status foo", in the case where the file would be +@c gotten except for Entries.Static, might say +@c something to distinguish this from other cases. +@c One thing that periodically gets suggested is to +@c have "cvs update" print something when it skips +@c files due to Entries.Static, but IMHO that kind of +@c noise pretty much makes the Entries.Static feature +@c useless. + +@cindex Tag file, in CVS directory +@cindex CVS/Tag file +@cindex Sticky tags/dates, per-directory +@cindex Per-directory sticky tags/dates +@item Tag +This file contains per-directory sticky tags or dates. +The first character is @samp{T} for a branch tag, +@samp{N} for a non-branch tag, or @samp{D} for a date, +or another character to mean the file should be +silently ignored, for future expansion. This character +is followed by the tag or date. Note that +per-directory sticky tags or dates are used for things +like applying to files which are newly added; they +might not be the same as the sticky tags or dates on +individual files. For general information on sticky +tags and dates, see @ref{Sticky tags}. +@c FIXME: This needs to be much better documented, +@c preferably not in the context of "working directory +@c storage". +@c FIXME: The Sticky tags node needs to discuss, or xref to +@c someplace which discusses, per-directory sticky +@c tags and the distinction with per-file sticky tags. + +@cindex Checkin.prog file, in CVS directory +@cindex CVS/Checkin.prog file +@cindex Update.prog file, in CVS directory +@cindex CVS/Update.prog file +@item Checkin.prog +@itemx Update.prog +These files store the programs specified by the +@samp{-i} and @samp{-u} options in the modules file, +respectively. + +@cindex Notify file, in CVS directory +@cindex CVS/Notify file +@item Notify +This file stores notifications (for example, for +@code{edit} or @code{unedit}) which have not yet been +sent to the server. Its format is not yet documented +here. + +@cindex Notify.tmp file, in CVS directory +@cindex CVS/Notify.tmp file +@item Notify.tmp +This file is to @file{Notify} as @file{Entries.Backup} +is to @file{Entries}. That is, to write @file{Notify}, +first write the new contents to @file{Notify.tmp} and +then (atomically where possible), rename it to +@file{Notify}. + +@cindex Base directory, in CVS directory +@cindex CVS/Base directory +@item Base +If watches are in use, then an @code{edit} command +stores the original copy of the file in the @file{Base} +directory. This allows the @code{unedit} command to +operate even if it is unable to communicate with the +server. + +@cindex Baserev file, in CVS directory +@cindex CVS/Baserev file +@item Baserev +The file lists the revision for each of the files in +the @file{Base} directory. The format is: + +@example +B@var{name}/@var{rev}/@var{expansion} +@end example + +where @var{expansion} should be ignored, to allow for +future expansion. + +@cindex Baserev.tmp file, in CVS directory +@cindex CVS/Baserev.tmp file +@item Baserev.tmp +This file is to @file{Baserev} as @file{Entries.Backup} +is to @file{Entries}. That is, to write @file{Baserev}, +first write the new contents to @file{Baserev.tmp} and +then (atomically where possible), rename it to +@file{Baserev}. + +@cindex Template file, in CVS directory +@cindex CVS/Template file +@item Template +This file contains the template specified by the +@file{rcsinfo} file (@pxref{rcsinfo}). It is only used +by the client; the non-client/server @sc{cvs} consults +@file{rcsinfo} directly. +@end table + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Intro administrative files +@section The administrative files +@cindex Administrative files (intro) +@cindex Modules file +@cindex CVSROOT, module name +@cindex Defining modules (intro) + +@c FIXME: this node should be reorganized into "general +@c information about admin files" and put the "editing +@c admin files" stuff up front rather than jumping into +@c the details of modules right away. Then the +@c Administrative files node can go away, the information +@c on each admin file distributed to a place appropriate +@c to its function, and this node can contain a table +@c listing each file and a @ref to its detailed description. + +The directory @file{$CVSROOT/CVSROOT} contains some @dfn{administrative +files}. @xref{Administrative files}, for a complete description. +You can use @sc{cvs} without any of these files, but +some commands work better when at least the +@file{modules} file is properly set up. + +The most important of these files is the @file{modules} +file. It defines all modules in the repository. This +is a sample @file{modules} file. + +@c FIXME: The CVSROOT line is a goofy example now that +@c mkmodules doesn't exist. +@example +CVSROOT CVSROOT +modules CVSROOT modules +cvs gnu/cvs +rcs gnu/rcs +diff gnu/diff +tc yoyodyne/tc +@end example + +The @file{modules} file is line oriented. In its +simplest form each line contains the name of the +module, whitespace, and the directory where the module +resides. The directory is a path relative to +@code{$CVSROOT}. The last four lines in the example +above are examples of such lines. + +@c FIXME: might want to introduce the concept of options in modules file +@c (the old example which was here, -i mkmodules, is obsolete). + +The line that defines the module called @samp{modules} +uses features that are not explained here. +@xref{modules}, for a full explanation of all the +available features. + +@c FIXME: subsection without node is bogus +@subsection Editing administrative files +@cindex Editing administrative files +@cindex Administrative files, editing them + +You edit the administrative files in the same way that you would edit +any other module. Use @samp{cvs checkout CVSROOT} to get a working +copy, edit it, and commit your changes in the normal way. + +It is possible to commit an erroneous administrative +file. You can often fix the error and check in a new +revision, but sometimes a particularly bad error in the +administrative file makes it impossible to commit new +revisions. +@c @xref{Bad administrative files} for a hint +@c about how to solve such situations. +@c -- administrative file checking-- + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Multiple repositories +@section Multiple repositories +@cindex Multiple repositories +@cindex Repositories, multiple +@cindex Many repositories +@cindex Parallel repositories +@cindex Disjoint repositories +@cindex CVSROOT, multiple repositories + +In some situations it is a good idea to have more than +one repository, for instance if you have two +development groups that work on separate projects +without sharing any code. All you have to do to have +several repositories is to specify the appropriate +repository, using the @code{CVSROOT} environment +variable, the @samp{-d} option to @sc{cvs}, or (once +you have checked out a working directory) by simply +allowing @sc{cvs} to use the repository that was used +to check out the working directory +(@pxref{Specifying a repository}). + +The big advantage of having multiple repositories is +that they can reside on different servers. With @sc{cvs} +version 1.10, a single command cannot recurse into +directories from different repositories. With development +versions of @sc{cvs}, you can check out code from multiple +servers into your working directory. @sc{cvs} will +recurse and handle all the details of making +connections to as many server machines as necessary to +perform the requested command. Here is an example of +how to set up a working directory: + +@example +cvs -d server1:/cvs co dir1 +cd dir1 +cvs -d server2:/root co sdir +cvs update +@end example + +The @code{cvs co} commands set up the working +directory, and then the @code{cvs update} command will +contact server2, to update the dir1/sdir subdirectory, +and server1, to update everything else. + +@c FIXME: Does the FAQ have more about this? I have a +@c dim recollection, but I'm too lazy to check right now. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Creating a repository +@section Creating a repository + +@cindex Repository, setting up +@cindex Creating a repository +@cindex Setting up a repository + +To set up a @sc{cvs} repository, first choose the +machine and disk on which you want to store the +revision history of the source files. CPU and memory +requirements are modest, so most machines should be +adequate. For details see @ref{Server requirements}. +@c Possible that we should be providing a quick rule of +@c thumb, like the 32M memory for the server. That +@c might increase the number of people who are happy +@c with the answer, without following the xref. + +To estimate disk space +requirements, if you are importing RCS files from +another system, the size of those files is the +approximate initial size of your repository, or if you +are starting without any version history, a rule of +thumb is to allow for the server approximately three +times the size of the code to be under CVS for the +repository (you will eventually outgrow this, but not +for a while). On the machines on which the developers +will be working, you'll want disk space for +approximately one working directory for each developer +(either the entire tree or a portion of it, depending +on what each developer uses). + +The repository should be accessible +(directly or via a networked file system) from all +machines which want to use @sc{cvs} in server or local +mode; the client machines need not have any access to +it other than via the @sc{cvs} protocol. It is not +possible to use @sc{cvs} to read from a repository +which one only has read access to; @sc{cvs} needs to be +able to create lock files (@pxref{Concurrency}). + +@cindex init (subcommand) +To create a repository, run the @code{cvs init} +command. It will set up an empty repository in the +@sc{cvs} root specified in the usual way +(@pxref{Repository}). For example, + +@example +cvs -d /usr/local/cvsroot init +@end example + +@code{cvs init} is careful to never overwrite any +existing files in the repository, so no harm is done if +you run @code{cvs init} on an already set-up +repository. + +@code{cvs init} will enable history logging; if you +don't want that, remove the history file after running +@code{cvs init}. @xref{history file}. + +@node Backing up +@section Backing up a repository +@cindex Repository, backing up +@cindex Backing up, repository + +There is nothing particularly magical about the files +in the repository; for the most part it is possible to +back them up just like any other files. However, there +are a few issues to consider. + +@cindex Locks, cvs, and backups +@cindex #cvs.rfl, and backups +The first is that to be paranoid, one should either not +use @sc{cvs} during the backup, or have the backup +program lock @sc{cvs} while doing the backup. To not +use @sc{cvs}, you might forbid logins to machines which +can access the repository, turn off your @sc{cvs} +server, or similar mechanisms. The details would +depend on your operating system and how you have +@sc{cvs} set up. To lock @sc{cvs}, you would create +@file{#cvs.rfl} locks in each repository directory. +See @ref{Concurrency}, for more on @sc{cvs} locks. +Having said all this, if you just back up without any +of these precautions, the results are unlikely to be +particularly dire. Restoring from backup, the +repository might be in an inconsistent state, but this +would not be particularly hard to fix manually. + +When you restore a repository from backup, assuming +that changes in the repository were made after the time +of the backup, working directories which were not +affected by the failure may refer to revisions which no +longer exist in the repository. Trying to run @sc{cvs} +in such directories will typically produce an error +message. One way to get those changes back into the +repository is as follows: + +@itemize @bullet +@item +Get a new working directory. + +@item +Copy the files from the working directory from before +the failure over to the new working directory (do not +copy the contents of the @file{CVS} directories, of +course). + +@item +Working in the new working directory, use commands such +as @code{cvs update} and @code{cvs diff} to figure out +what has changed, and then when you are ready, commit +the changes into the repository. +@end itemize + +@node Moving a repository +@section Moving a repository +@cindex Repository, moving +@cindex Moving a repository +@cindex Copying a repository + +Just as backing up the files in the repository is +pretty much like backing up any other files, if you +need to move a repository from one place to another it +is also pretty much like just moving any other +collection of files. + +The main thing to consider is that working directories +point to the repository. The simplest way to deal with +a moved repository is to just get a fresh working +directory after the move. Of course, you'll want to +make sure that the old working directory had been +checked in before the move, or you figured out some +other way to make sure that you don't lose any +changes. If you really do want to reuse the existing +working directory, it should be possible with manual +surgery on the @file{CVS/Repository} files. You can +see @ref{Working directory storage}, for information on +the @file{CVS/Repository} and @file{CVS/Root} files, but +unless you are sure you want to bother, it probably +isn't worth it. +@c FIXME: Surgery on CVS/Repository should be avoided +@c by making RELATIVE_REPOS the default. +@c FIXME-maybe: might want some documented way to +@c change the CVS/Root files in some particular tree. +@c But then again, I don't know, maybe just having +@c people do this in perl/shell/&c isn't so bad... + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Remote repositories +@section Remote repositories +@cindex Repositories, remote +@cindex Remote repositories +@cindex Client/Server Operation +@cindex Server, CVS + + Your working copy of the sources can be on a +different machine than the repository. Using @sc{cvs} +in this manner is known as @dfn{client/server} +operation. You run @sc{cvs} on a machine which can +mount your working directory, known as the +@dfn{client}, and tell it to communicate to a machine +which can mount the repository, known as the +@dfn{server}. Generally, using a remote +repository is just like using a local one, except that +the format of the repository name is: + +@example +:@var{method}:@var{user}@@@var{hostname}:/path/to/repository +@end example + +The details of exactly what needs to be set up depend +on how you are connecting to the server. + +If @var{method} is not specified, and the repository +name contains @samp{:}, then the default is @code{ext} +or @code{server}, depending on your platform; both are +described in @ref{Connecting via rsh}. +@c Should we try to explain which platforms are which? +@c Platforms like unix and VMS, which only allow +@c privileged programs to bind to sockets <1024 lose on +@c :server: +@c Platforms like Mac and VMS, whose rsh program is +@c unusable or nonexistent, lose on :ext: +@c Platforms like OS/2 and NT probably could plausibly +@c default either way (modulo -b troubles). + +@c FIXME: We need to have a better way of explaining +@c what method to use. This presentation totally +@c obscures the fact that :ext: and CVS_RSH is the way to +@c use SSH, for example. Plus it incorrectly implies +@c that you need an @code{rsh} binary on the client to use +@c :server:. +@c Also note that rsh not pserver is the right choice if you want +@c users to be able to create their own repositories +@c (because of the --allow-root related issues). +@menu +* Server requirements:: Memory and other resources for servers +* Connecting via rsh:: Using the @code{rsh} program to connect +* Password authenticated:: Direct connections using passwords +* GSSAPI authenticated:: Direct connections using GSSAPI +* Kerberos authenticated:: Direct connections with kerberos +* Connecting via fork:: Using a forked @code{cvs server} to connect +@end menu + +@node Server requirements +@subsection Server requirements + +The quick answer to what sort of machine is suitable as +a server is that requirements are modest---a server +with 32M of memory or even less can handle a fairly +large source tree with a fair amount of activity. +@c Say something about CPU speed too? I'm even less sure +@c what to say on that subject... + +The real answer, of course, is more complicated. +Estimating the known areas of large memory consumption +should be sufficient to estimate memory requirements. +There are two such areas documented here; other memory +consumption should be small by comparison (if you find +that is not the case, let us know, as described in +@ref{BUGS}, so we can update this documentation). + +The first area of big memory consumption is large +checkouts, when using the @sc{cvs} server. The server +consists of two processes for each client that it is +serving. Memory consumption on the child process +should remain fairly small. Memory consumption on the +parent process, particularly if the network connection +to the client is slow, can be expected to grow to +slightly more than the size of the sources in a single +directory, or two megabytes, whichever is larger. +@c "two megabytes" of course is SERVER_HI_WATER. But +@c we don't mention that here because we are +@c documenting the default configuration of CVS. If it +@c is a "standard" thing to change that value, it +@c should be some kind of run-time configuration. +@c +@c See cvsclient.texi for more on the design decision +@c to not have locks in place while waiting for the +@c client, which is what results in memory consumption +@c as high as this. + +Multiplying the size of each @sc{cvs} server by the +number of servers which you expect to have active at +one time should give an idea of memory requirements for +the server. For the most part, the memory consumed by +the parent process probably can be swap space rather +than physical memory. +@c Has anyone verified that notion about swap space? +@c I say it based pretty much on guessing that the +@c ->text of the struct buffer_data only gets accessed +@c in a first in, first out fashion, but I haven't +@c looked very closely. + +@c What about disk usage in /tmp on the server? I think that +@c it can be substantial, but I haven't looked at this +@c again and tried to figure it out ("cvs import" is +@c probably the worst case...). + +The second area of large memory consumption is +@code{diff}, when checking in large files. This is +required even for binary files. The rule of thumb is +to allow about ten times the size of the largest file +you will want to check in, although five times may be +adequate. For example, if you want to check in a file +which is 10 megabytes, you should have 100 megabytes of +memory on the machine doing the checkin (the server +machine for client/server, or the machine running +@sc{cvs} for non-client/server). This can be swap +space rather than physical memory. Because the memory +is only required briefly, there is no particular need +to allow memory for more than one such checkin at a +time. +@c The 5-10 times rule of thumb is from Paul Eggert for +@c GNU diff. I don't think it is in the GNU diff +@c manual or anyplace like that. +@c +@c Probably we could be saying more about +@c non-client/server CVS. +@c I would guess for non-client/server CVS in an NFS +@c environment the biggest issues are the network and +@c the NFS server. + +Resource consumption for the client is even more +modest---any machine with enough capacity to run the +operating system in question should have little +trouble. +@c Is that true? I think the client still wants to +@c (bogusly) store entire files in memory at times. + +For information on disk space requirements, see +@ref{Creating a repository}. + +@node Connecting via rsh +@subsection Connecting with rsh + +@cindex rsh +CVS uses the @file{rsh} protocol to perform these +operations, so the remote user host needs to have a +@file{.rhosts} file which grants access to the local +user. + +For example, suppose you are the user @file{mozart} on +the local machine @file{toe.example.com}, and the +server machine is @file{faun.example.org}. On +faun, put the following line into the file +@file{.rhosts} in @file{bach}'s home directory: + +@example +toe.example.com mozart +@end example + +Then test that @code{rsh} is working with + +@example +rsh -l bach faun.example.org 'echo $PATH' +@end example + +@cindex CVS_SERVER, environment variable +Next you have to make sure that @code{rsh} will be able +to find the server. Make sure that the path which +@code{rsh} printed in the above example includes the +directory containing a program named @code{cvs} which +is the server. You need to set the path in +@file{.bashrc}, @file{.cshrc}, etc., not @file{.login} +or @file{.profile}. Alternately, you can set the +environment variable @code{CVS_SERVER} on the client +machine to the filename of the server you want to use, +for example @file{/usr/local/bin/cvs-1.6}. +@c FIXME: there should be a way to specify the +@c program in CVSROOT, not CVS_SERVER, so that one can use +@c different ones for different roots. e.g. ":server;cvs=cvs-1.6:" +@c instead of ":server:". + +There is no need to edit @file{inetd.conf} or start a +@sc{cvs} server daemon. + +@cindex :server:, setting up +@cindex :ext:, setting up +@cindex Kerberos, using kerberized rsh +@cindex SSH (rsh replacement) +@cindex rsh replacements (Kerberized, SSH, &c) +There are two access methods that you use in @code{CVSROOT} +for rsh. @code{:server:} specifies an internal rsh +client, which is supported only by some CVS ports. +@code{:ext:} specifies an external rsh program. By +default this is @code{rsh} but you may set the +@code{CVS_RSH} environment variable to invoke another +program which can access the remote server (for +example, @code{remsh} on HP-UX 9 because @code{rsh} is +something different). It must be a program which can +transmit data to and from the server without modifying +it; for example the Windows NT @code{rsh} is not +suitable since it by default translates between CRLF +and LF. The OS/2 CVS port has a hack to pass @samp{-b} +to @code{rsh} to get around this, but since this could +potentially cause problems for programs other than the +standard @code{rsh}, it may change in the future. If +you set @code{CVS_RSH} to @code{SSH} or some other rsh +replacement, the instructions in the rest of this +section concerning @file{.rhosts} and so on are likely +to be inapplicable; consult the documentation for your rsh +replacement. +@c FIXME: there should be a way to specify the +@c program in CVSROOT, not CVS_RSH, so that one can use +@c different ones for different roots. e.g. ":ext;rsh=remsh:" +@c instead of ":ext:". +@c See also the comment in src/client.c for rationale +@c concerning "rsh" being the default and never +@c "remsh". + +Continuing our example, supposing you want to access +the module @file{foo} in the repository +@file{/usr/local/cvsroot/}, on machine +@file{faun.example.org}, you are ready to go: + +@example +cvs -d :ext:bach@@faun.example.org:/usr/local/cvsroot checkout foo +@end example + +(The @file{bach@@} can be omitted if the username is +the same on both the local and remote hosts.) + +@c Should we mention "rsh host echo hi" and "rsh host +@c cat" (the latter followed by typing text and ^D) +@c as troubleshooting techniques? Probably yes +@c (people tend to have trouble setting this up), +@c but this kind of thing can be hard to spell out. + +@node Password authenticated +@subsection Direct connection with password authentication + +The @sc{cvs} client can also connect to the server +using a password protocol. This is particularly useful +if using @code{rsh} is not feasible (for example, +the server is behind a firewall), and Kerberos also is +not available. + + To use this method, it is necessary to make +some adjustments on both the server and client sides. + +@menu +* Password authentication server:: Setting up the server +* Password authentication client:: Using the client +* Password authentication security:: What this method does and does not do +@end menu + +@node Password authentication server +@subsubsection Setting up the server for password authentication + +First of all, you probably want to tighten the +permissions on the @file{$CVSROOT} and +@file{$CVSROOT/CVSROOT} directories. See @ref{Password +authentication security}, for more details. + +@cindex pserver (subcommand) +@cindex Password server, setting up +@cindex Authenticating server, setting up +@c FIXME: this isn't quite right regarding port +@c numbers; CVS looks up "cvspserver" in +@c /etc/services (on unix, but what about non-unix?). +On the server side, the file @file{/etc/inetd.conf} +needs to be edited so @code{inetd} knows to run the +command @code{cvs pserver} when it receives a +connection on the right port. By default, the port +number is 2401; it would be different if your client +were compiled with @code{CVS_AUTH_PORT} defined to +something else, though. + + If your @code{inetd} allows raw port numbers in +@file{/etc/inetd.conf}, then the following (all on a +single line in @file{inetd.conf}) should be sufficient: + +@example +2401 stream tcp nowait root /usr/local/bin/cvs +cvs -f --allow-root=/usr/cvsroot pserver +@end example + +You could also use the +@samp{-T} option to specify a temporary directory. + +The @samp{--allow-root} option specifies the allowable +@sc{cvsroot} directory. Clients which attempt to use a +different @sc{cvsroot} directory will not be allowed to +connect. If there is more than one @sc{cvsroot} +directory which you want to allow, repeat the option. +(Unfortunately, many versions of @code{inetd} have very small +limits on the number of arguments and/or the total length +of the command. The usual solution to this problem is +to have @code{inetd} run a shell script which then invokes +@sc{cvs} with the necessary arguments.) + + If your @code{inetd} wants a symbolic service +name instead of a raw port number, then put this in +@file{/etc/services}: + +@example +cvspserver 2401/tcp +@end example + + and put @code{cvspserver} instead of +@code{2401} in @file{inetd.conf}. + + Once the above is taken care of, restart your +@code{inetd}, or do whatever is necessary to force it +to reread its initialization files. + +If you are having trouble setting this up, see +@ref{Connection}. + +@cindex CVS passwd file +@cindex passwd (admin file) +Because the client stores and transmits passwords in +cleartext (almost---see @ref{Password authentication +security}, for details), a separate @sc{cvs} password +file is generally used, so people don't compromise +their regular passwords when they access the +repository. This file is +@file{$CVSROOT/CVSROOT/passwd} (@pxref{Intro +administrative files}). It uses a colon-separated +format, similar to @file{/etc/passwd} on Unix systems, +except that it has fewer fields: @sc{cvs} username, +optional password, and an optional system username for +@sc{cvs} to run as if authentication succeeds. Here is +an example @file{passwd} file with five entries: + +@example +anonymous: +bach:ULtgRLXo7NRxs +spwang:1sOp854gDF3DY +melissa:tGX1fS8sun6rY:pubcvs +qproj:XR4EZcEs0szik:pubcvs +@end example + +(The passwords are encrypted according to the standard +Unix @code{crypt()} function, so it is possible to +paste in passwords directly from regular Unix +@file{/etc/passwd} files.) + +The first line in the example will grant access to any +@sc{cvs} client attempting to authenticate as user +@code{anonymous}, no matter what password they use, +including an empty password. (This is typical for +sites granting anonymous read-only access; for +information on how to do the "read-only" part, see +@xref{Read-only access}.) + +The second and third lines will grant access to +@code{bach} and @code{spwang} if they supply their +respective plaintext passwords. + +@cindex User aliases +The fourth line will grant access to @code{melissa}, if +she supplies the correct password, but her @sc{cvs} +operations will actually run on the server side under +the system user @code{pubcvs}. Thus, there need not be +any system user named @code{melissa}, but there +@emph{must} be one named @code{pubcvs}. + +The fifth line shows that system user identities can be +shared: any client who successfully authenticates as +@code{qproj} will actually run as @code{pubcvs}, just +as @code{melissa} does. That way you could create a +single, shared system user for each project in your +repository, and give each developer their own line in +the @file{$CVSROOT/CVSROOT/passwd} file. The @sc{cvs} +username on each line would be different, but the +system username would be the same. The reason to have +different @sc{cvs} usernames is that CVS will log their +actions under those names: when @code{melissa} commits +a change to a project, the checkin is recorded in the +project's history under the name @code{melissa}, not +@code{pubcvs}. And the reason to have them share a +system username is so that you can arrange permissions +in the relevant area of the repository such that only +that account has write-permission there. + +If the system-user field is present, all +password-authenticated @sc{cvs} commands run as that +user; if no system user is specified, @sc{cvs} simply +takes the @sc{cvs} username as the system username and +runs commands as that user. In either case, if there +is no such user on the system, then the @sc{cvs} +operation will fail (regardless of whether the client +supplied a valid password). + +The password and system-user fields can both be omitted +(and if the system-user field is omitted, then also +omit the colon that would have separated it from the +encrypted password). For example, this would be a +valid @file{$CVSROOT/CVSROOT/passwd} file: + +@example +anonymous::pubcvs +fish:rKa5jzULzmhOo:kfogel +sussman:1sOp854gDF3DY +@end example + +When the password field is omitted or empty, then the +client's authentication attempt will succeed with any +password, including the empty string. However, the +colon after the @sc{cvs} username is always necessary, +even if the password is empty. + +CVS can also fall back to use system authentication. +When authenticating a password, the server first checks +for the user in the @file{$CVSROOT/CVSROOT/passwd} +file. If it finds the user, it will use that entry for +authentication as described above. But if it does not +find the user, or if the @sc{cvs} @file{passwd} file +does not exist, then the server can try to authenticate +the username and password using the operating system's +user-lookup routines (this "fallback" behavior can be +disabled by setting @code{SystemAuth=no} in the +@sc{cvs} @file{config} file, @pxref{config}). Be +aware, however, that falling back to system +authentication might be a security risk: @sc{cvs} +operations would then be authenticated with that user's +regular login password, and the password flies across +the network in plaintext. See @ref{Password +authentication security} for more on this. + +Right now, the only way to put a password in the +@sc{cvs} @file{passwd} file is to paste it there from +somewhere else. Someday, there may be a @code{cvs +passwd} command. + +Unlike many of the files in @file{$CVSROOT/CVSROOT}, it +is normal to edit the @file{passwd} file in-place, +rather than via @sc{cvs}. This is because of the +possible security risks of having the @file{passwd} +file checked out to people's working copies. If you do +want to include the @file{passwd} file in checkouts of +@file{$CVSROOT/CVSROOT}, see @xref{checkoutlist}. + +@c We might also suggest using the @code{htpasswd} command +@c from freely available web servers as well, but that +@c would open up a can of worms in that the users next +@c questions are likely to be "where do I get it?" and +@c "how do I use it?" +@c Also note that htpasswd, at least the version I had, +@c likes to clobber the third field. + +@node Password authentication client +@subsubsection Using the client with password authentication +@cindex Login (subcommand) +@cindex Password client, using +@cindex Authenticated client, using +@cindex :pserver:, setting up +To run a @sc{cvs} command on a remote repository via +the password-authenticating server, one specifies the +@code{pserver} protocol, username, repository host, and +path to the repository. For example: + +@example +cvs -d :pserver:bach@@faun.example.org:/usr/local/cvsroot checkout someproj +@end example + +or + +@example +CVSROOT=:pserver:bach@@faun.example.org:/usr/local/cvsroot +cvs checkout someproj +@end example + +However, unless you're connecting to a public-access +repository (i.e., one where that username doesn't +require a password), you'll need to @dfn{log in} first. +Logging in verifies your password with the repository. +It's done with the @code{login} command, which will +prompt you interactively for the password: + +@example +cvs -d :pserver:bach@@faun.example.org:/usr/local/cvsroot login +CVS password: +@end example + +After you enter the password, @sc{cvs} verifies it with +the server. If the verification succeeds, then that +combination of username, host, repository, and password +is permanently recorded, so future transactions with +that repository won't require you to run @code{cvs +login}. (If verification fails, @sc{cvs} will exit +complaining that the password was incorrect, and +nothing will be recorded.) + +The records are stored, by default, in the file +@file{$HOME/.cvspass}. That file's format is +human-readable, and to a degree human-editable, but +note that the passwords are not stored in +cleartext---they are trivially encoded to protect them +from "innocent" compromise (i.e., inadvertent viewing +by a system administrator or other non-malicious +person). + +@cindex CVS_PASSFILE, environment variable +You can change the default location of this file by +setting the @code{CVS_PASSFILE} environment variable. +If you use this variable, make sure you set it +@emph{before} @code{cvs login} is run. If you were to +set it after running @code{cvs login}, then later +@sc{cvs} commands would be unable to look up the +password for transmission to the server. + +Once you have logged in, all @sc{cvs} commands using +that remote repository and username will authenticate +with the stored password. So, for example + +@example +cvs -d :pserver:bach@@faun.example.org:/usr/local/cvsroot checkout foo +@end example + +should just work (unless the password changes on the +server side, in which case you'll have to re-run +@code{cvs login}). + +Note that if the @samp{:pserver:} were not present in +the repository specification, @sc{cvs} would assume it +should use @code{rsh} to connect with the server +instead (@pxref{Connecting via rsh}). + +Of course, once you have a working copy checked out and +are running @sc{cvs} commands from within it, there is +no longer any need to specify the repository +explicitly, because @sc{cvs} can deduce the repository +from the working copy's @file{CVS} subdirectory. + +@c FIXME: seems to me this needs somewhat more +@c explanation. +@cindex Logout (subcommand) +The password for a given remote repository can be +removed from the @code{CVS_PASSFILE} by using the +@code{cvs logout} command. + +@node Password authentication security +@subsubsection Security considerations with password authentication + +@cindex Security, of pserver +The passwords are stored on the client side in a +trivial encoding of the cleartext, and transmitted in +the same encoding. The encoding is done only to +prevent inadvertent password compromises (i.e., a +system administrator accidentally looking at the file), +and will not prevent even a naive attacker from gaining +the password. + +@c FIXME: The bit about "access to the repository +@c implies general access to the system is *not* specific +@c to pserver; it applies to kerberos and SSH and +@c everything else too. Should reorganize the +@c documentation to make this clear. +The separate @sc{cvs} password file (@pxref{Password +authentication server}) allows people +to use a different password for repository access than +for login access. On the other hand, once a user has +non-read-only +access to the repository, she can execute programs on +the server system through a variety of means. Thus, repository +access implies fairly broad system access as well. It +might be possible to modify @sc{cvs} to prevent that, +but no one has done so as of this writing. +@c OpenBSD uses chroot() and copies the repository to +@c provide anonymous read-only access (for details see +@c http://www.openbsd.org/anoncvs.shar). While this +@c closes the most obvious holes, I'm not sure it +@c closes enough holes to recommend it (plus it is +@c *very* easy to accidentally screw up a setup of this +@c type). + +Note that because the @file{$CVSROOT/CVSROOT} directory +contains @file{passwd} and other files which are used +to check security, you must control the permissions on +this directory as tightly as the permissions on +@file{/etc}. The same applies to the @file{$CVSROOT} +directory itself and any directory +above it in the tree. Anyone who has write access to +such a directory will have the ability to become any +user on the system. Note that these permissions are +typically tighter than you would use if you are not +using pserver. +@c TODO: Would be really nice to document/implement a +@c scheme where the CVS server can run as some non-root +@c user, e.g. "cvs". CVSROOT/passwd would contain a +@c bunch of entries of the form foo:xxx:cvs (or the "cvs" +@c would be implicit). This would greatly reduce +@c security risks such as those hinted at in the +@c previous paragraph. I think minor changes to CVS +@c might be required but mostly this would just need +@c someone who wants to play with it, document it, &c. + +In summary, anyone who gets the password gets +repository access (which may imply some measure of general system +access as well). The password is available to anyone +who can sniff network packets or read a protected +(i.e., user read-only) file. If you want real +security, get Kerberos. + +@node GSSAPI authenticated +@subsection Direct connection with GSSAPI + +@cindex GSSAPI +@cindex Security, GSSAPI +@cindex :gserver:, setting up +@cindex Kerberos, using :gserver: +GSSAPI is a generic interface to network security +systems such as Kerberos 5. +If you have a working GSSAPI library, you can have +@sc{cvs} connect via a direct @sc{tcp} connection, +authenticating with GSSAPI. + +To do this, @sc{cvs} needs to be compiled with GSSAPI +support; when configuring @sc{cvs} it tries to detect +whether GSSAPI libraries using kerberos version 5 are +present. You can also use the @file{--with-gssapi} +flag to configure. + +The connection is authenticated using GSSAPI, but the +message stream is @emph{not} authenticated by default. +You must use the @code{-a} global option to request +stream authentication. + +The data transmitted is @emph{not} encrypted by +default. Encryption support must be compiled into both +the client and the server; use the +@file{--enable-encrypt} configure option to turn it on. +You must then use the @code{-x} global option to +request encryption. + +GSSAPI connections are handled on the server side by +the same server which handles the password +authentication server; see @ref{Password authentication +server}. If you are using a GSSAPI mechanism such as +Kerberos which provides for strong authentication, you +will probably want to disable the ability to +authenticate via cleartext passwords. To do so, create +an empty @file{CVSROOT/passwd} password file, and set +@code{SystemAuth=no} in the config file +(@pxref{config}). + +The GSSAPI server uses a principal name of +cvs/@var{hostname}, where @var{hostname} is the +canonical name of the server host. You will have to +set this up as required by your GSSAPI mechanism. + +To connect using GSSAPI, use @samp{:gserver:}. For +example, + +@example +cvs -d :gserver:faun.example.org:/usr/local/cvsroot checkout foo +@end example + +@node Kerberos authenticated +@subsection Direct connection with kerberos + +@cindex Kerberos, using :kserver: +@cindex Security, kerberos +@cindex :kserver:, setting up +The easiest way to use kerberos is to use the kerberos +@code{rsh}, as described in @ref{Connecting via rsh}. +The main disadvantage of using rsh is that all the data +needs to pass through additional programs, so it may be +slower. So if you have kerberos installed you can +connect via a direct @sc{tcp} connection, +authenticating with kerberos. + +This section concerns the kerberos network security +system, version 4. Kerberos version 5 is supported via +the GSSAPI generic network security interface, as +described in the previous section. + +To do this, @sc{cvs} needs to be compiled with kerberos +support; when configuring @sc{cvs} it tries to detect +whether kerberos is present or you can use the +@file{--with-krb4} flag to configure. + +The data transmitted is @emph{not} encrypted by +default. Encryption support must be compiled into both +the client and server; use the +@file{--enable-encryption} configure option to turn it +on. You must then use the @code{-x} global option to +request encryption. + +@cindex CVS_CLIENT_PORT +You need to edit @file{inetd.conf} on the server +machine to run @code{cvs kserver}. The client uses +port 1999 by default; if you want to use another port +specify it in the @code{CVS_CLIENT_PORT} environment +variable on the client. + +@cindex kinit +When you want to use @sc{cvs}, get a ticket in the +usual way (generally @code{kinit}); it must be a ticket +which allows you to log into the server machine. Then +you are ready to go: + +@example +cvs -d :kserver:faun.example.org:/usr/local/cvsroot checkout foo +@end example + +Previous versions of @sc{cvs} would fall back to a +connection via rsh; this version will not do so. + +@node Connecting via fork +@subsection Connecting with fork + +@cindex fork, access method +@cindex :fork:, setting up +This access method allows you to connect to a +repository on your local disk via the remote protocol. +In other words it does pretty much the same thing as +@code{:local:}, but various quirks, bugs and the like are +those of the remote @sc{cvs} rather than the local +@sc{cvs}. + +For day-to-day operations you might prefer either +@code{:local:} or @code{:fork:}, depending on your +preferences. Of course @code{:fork:} comes in +particularly handy in testing or +debugging @code{cvs} and the remote protocol. +Specifically, we avoid all of the network-related +setup/configuration, timeouts, and authentication +inherent in the other remote access methods but still +create a connection which uses the remote protocol. + +To connect using the @code{fork} method, use +@samp{:fork:} and the pathname to your local +repository. For example: + +@example +cvs -d :fork:/usr/local/cvsroot checkout foo +@end example + +@cindex CVS_SERVER, and :fork: +As with @code{:ext:}, the server is called @samp{cvs} +by default, or the value of the @code{CVS_SERVER} +environment variable. + +@c --------------------------------------------------------------------- +@node Read-only access +@section Read-only repository access +@cindex Read-only repository access +@cindex readers (admin file) +@cindex writers (admin file) + + It is possible to grant read-only repository +access to people using the password-authenticated +server (@pxref{Password authenticated}). (The +other access methods do not have explicit support for +read-only users because those methods all assume login +access to the repository machine anyway, and therefore +the user can do whatever local file permissions allow +her to do.) + + A user who has read-only access can do only +those @sc{cvs} operations which do not modify the +repository, except for certain ``administrative'' files +(such as lock files and the history file). It may be +desirable to use this feature in conjunction with +user-aliasing (@pxref{Password authentication server}). + +Unlike with previous versions of @sc{cvs}, read-only +users should be able merely to read the repository, and +not to execute programs on the server or otherwise gain +unexpected levels of access. Or to be more accurate, +the @emph{known} holes have been plugged. Because this +feature is new and has not received a comprehensive +security audit, you should use whatever level of +caution seems warranted given your attitude concerning +security. + + There are two ways to specify read-only access +for a user: by inclusion, and by exclusion. + + "Inclusion" means listing that user +specifically in the @file{$CVSROOT/CVSROOT/readers} +file, which is simply a newline-separated list of +users. Here is a sample @file{readers} file: + +@example +melissa +splotnik +jrandom +@end example + + (Don't forget the newline after the last user.) + + "Exclusion" means explicitly listing everyone +who has @emph{write} access---if the file + +@example +$CVSROOT/CVSROOT/writers +@end example + +@noindent +exists, then only +those users listed in it have write access, and +everyone else has read-only access (of course, even the +read-only users still need to be listed in the +@sc{cvs} @file{passwd} file). The +@file{writers} file has the same format as the +@file{readers} file. + + Note: if your @sc{cvs} @file{passwd} +file maps cvs users onto system users (@pxref{Password +authentication server}), make sure you deny or grant +read-only access using the @emph{cvs} usernames, not +the system usernames. That is, the @file{readers} and +@file{writers} files contain cvs usernames, which may +or may not be the same as system usernames. + + Here is a complete description of the server's +behavior in deciding whether to grant read-only or +read-write access: + + If @file{readers} exists, and this user is +listed in it, then she gets read-only access. Or if +@file{writers} exists, and this user is NOT listed in +it, then she also gets read-only access (this is true +even if @file{readers} exists but she is not listed +there). Otherwise, she gets full read-write access. + + Of course there is a conflict if the user is +listed in both files. This is resolved in the more +conservative way, it being better to protect the +repository too much than too little: such a user gets +read-only access. + +@node Server temporary directory +@section Temporary directories for the server +@cindex Temporary directories, and server +@cindex Server, temporary directories + +While running, the @sc{cvs} server creates temporary +directories. They are named + +@example +cvs-serv@var{pid} +@end example + +@noindent +where @var{pid} is the process identification number of +the server. They are located in the directory +specified by the @code{TMPDIR} environment variable +(@pxref{Environment variables}), the @samp{-T} global +option (@pxref{Global options}), or failing that +@file{/tmp}. + +In most cases the server will remove the temporary +directory when it is done, whether it finishes normally +or abnormally. However, there are a few cases in which +the server does not or cannot remove the temporary +directory, for example: + +@itemize @bullet +@item +If the server aborts due to an internal server error, +it may preserve the directory to aid in debugging + +@item +If the server is killed in a way that it has no way of +cleaning up (most notably, @samp{kill -KILL} on unix). + +@item +If the system shuts down without an orderly shutdown, +which tells the server to clean up. +@end itemize + +In cases such as this, you will need to manually remove +the @file{cvs-serv@var{pid}} directories. As long as +there is no server running with process identification +number @var{pid}, it is safe to do so. + +@c --------------------------------------------------------------------- +@node Starting a new project +@chapter Starting a project with CVS +@cindex Starting a project with CVS +@cindex Creating a project + +@comment --moduledb-- +Because renaming files and moving them between +directories is somewhat inconvenient, the first thing +you do when you start a new project should be to think +through your file organization. It is not impossible +to rename or move files, but it does increase the +potential for confusion and @sc{cvs} does have some +quirks particularly in the area of renaming +directories. @xref{Moving files}. + +What to do next depends on the situation at hand. + +@menu +* Setting up the files:: Getting the files into the repository +* Defining the module:: How to make a module of the files +@end menu +@c -- File permissions! + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Setting up the files +@section Setting up the files + +The first step is to create the files inside the repository. This can +be done in a couple of different ways. + +@c -- The contributed scripts +@menu +* From files:: This method is useful with old projects + where files already exists. +* From other version control systems:: Old projects where you want to + preserve history from another system. +* From scratch:: Creating a directory tree from scratch. +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node From files +@subsection Creating a directory tree from a number of files +@cindex Importing files + +When you begin using @sc{cvs}, you will probably already have several +projects that can be +put under @sc{cvs} control. In these cases the easiest way is to use the +@code{import} command. An example is probably the easiest way to +explain how to use it. If the files you want to install in +@sc{cvs} reside in @file{@var{wdir}}, and you want them to appear in the +repository as @file{$CVSROOT/yoyodyne/@var{rdir}}, you can do this: + +@example +$ cd @var{wdir} +$ cvs import -m "Imported sources" yoyodyne/@var{rdir} yoyo start +@end example + +Unless you supply a log message with the @samp{-m} +flag, @sc{cvs} starts an editor and prompts for a +message. The string @samp{yoyo} is a @dfn{vendor tag}, +and @samp{start} is a @dfn{release tag}. They may fill +no purpose in this context, but since @sc{cvs} requires +them they must be present. @xref{Tracking sources}, for +more information about them. + +You can now verify that it worked, and remove your +original source directory. +@c FIXME: Need to say more about "verify that it +@c worked". What should the user look for in the output +@c from "diff -r"? + +@example +$ cd .. +$ mv @var{dir} @var{dir}.orig +$ cvs checkout yoyodyne/@var{dir} # @r{Explanation below} +$ diff -r @var{dir}.orig yoyodyne/@var{dir} +$ rm -r @var{dir}.orig +@end example + +@noindent +Erasing the original sources is a good idea, to make sure that you do +not accidentally edit them in @var{dir}, bypassing @sc{cvs}. +Of course, it would be wise to make sure that you have +a backup of the sources before you remove them. + +The @code{checkout} command can either take a module +name as argument (as it has done in all previous +examples) or a path name relative to @code{$CVSROOT}, +as it did in the example above. + +It is a good idea to check that the permissions +@sc{cvs} sets on the directories inside @code{$CVSROOT} +are reasonable, and that they belong to the proper +groups. @xref{File permissions}. + +If some of the files you want to import are binary, you +may want to use the wrappers features to specify which +files are binary and which are not. @xref{Wrappers}. + +@c The node name is too long, but I am having trouble +@c thinking of something more concise. +@node From other version control systems +@subsection Creating Files From Other Version Control Systems +@cindex Importing files, from other version control systems + +If you have a project which you are maintaining with +another version control system, such as @sc{rcs}, you +may wish to put the files from that project into +@sc{cvs}, and preserve the revision history of the +files. + +@table @asis +@cindex RCS, importing files from +@item From RCS +If you have been using @sc{rcs}, find the @sc{rcs} +files---usually a file named @file{foo.c} will have its +@sc{rcs} file in @file{RCS/foo.c,v} (but it could be +other places; consult the @sc{rcs} documentation for +details). Then create the appropriate directories in +@sc{cvs} if they do not already exist. Then copy the +files into the appropriate directories in the @sc{cvs} +repository (the name in the repository must be the name +of the source file with @samp{,v} added; the files go +directly in the appropriate directory of the repository, +not in an @file{RCS} subdirectory). This is one of the +few times when it is a good idea to access the @sc{cvs} +repository directly, rather than using @sc{cvs} +commands. Then you are ready to check out a new +working directory. +@c Someday there probably should be a "cvs import -t +@c rcs" or some such. It could even create magic +@c branches. It could also do something about the case +@c where the RCS file had a (non-magic) "0" branch. + +The @sc{rcs} file should not be locked when you move it +into @sc{cvs}; if it is, @sc{cvs} will have trouble +letting you operate on it. +@c What is the easiest way to unlock your files if you +@c have them locked? Especially if you have a lot of them? +@c This is a CVS bug/misfeature; importing RCS files +@c should ignore whether they are locked and leave them in +@c an unlocked state. Yet another reason for a separate +@c "import RCS file" command. + +@c How many is "many"? Or do they just import RCS files? +@item From another version control system +Many version control systems have the ability to export +@sc{rcs} files in the standard format. If yours does, +export the @sc{rcs} files and then follow the above +instructions. + +Failing that, probably your best bet is to write a +script that will check out the files one revision at a +time using the command line interface to the other +system, and then check the revisions into @sc{cvs}. +The @file{sccs2rcs} script mentioned below may be a +useful example to follow. + +@cindex SCCS, importing files from +@item From SCCS +There is a script in the @file{contrib} directory of +the @sc{cvs} source distribution called @file{sccs2rcs} +which converts @sc{sccs} files to @sc{rcs} files. +Note: you must run it on a machine which has both +@sc{sccs} and @sc{rcs} installed, and like everything +else in contrib it is unsupported (your mileage may +vary). + +@cindex PVCS, importing files from +@item From PVCS +There is a script in the @file{contrib} directory of +the @sc{cvs} source distribution called @file{pvcs_to_rcs} +which converts @sc{pvcs} archives to @sc{rcs} files. +You must run it on a machine which has both +@sc{pvcs} and @sc{rcs} installed, and like everything +else in contrib it is unsupported (your mileage may +vary). See the comments in the script for details. +@end table +@c CMZ and/or PATCHY were systems that were used in the +@c high energy physics community (especially for +@c CERNLIB). CERN has replaced them with CVS, but the +@c CAR format seems to live on as a way to submit +@c changes. There is a program car2cvs which converts +@c but I'm not sure where one gets a copy. +@c Not sure it is worth mentioning here, since it would +@c appear to affect only one particular community. +@c Best page for more information is: +@c http://wwwcn1.cern.ch/asd/cvs/index.html +@c See also: +@c http://ecponion.cern.ch/ecpsa/cernlib.html + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node From scratch +@subsection Creating a directory tree from scratch + +@c Also/instead should be documenting +@c $ cvs co -l . +@c $ mkdir tc +@c $ cvs add tc +@c $ cd tc +@c $ mkdir man +@c $ cvs add man +@c etc. +@c Using import to create the directories only is +@c probably a somewhat confusing concept. +For a new project, the easiest thing to do is probably +to create an empty directory structure, like this: + +@example +$ mkdir tc +$ mkdir tc/man +$ mkdir tc/testing +@end example + +After that, you use the @code{import} command to create +the corresponding (empty) directory structure inside +the repository: + +@example +$ cd tc +$ cvs import -m "Created directory structure" yoyodyne/@var{dir} yoyo start +@end example + +Then, use @code{add} to add files (and new directories) +as they appear. + +Check that the permissions @sc{cvs} sets on the +directories inside @code{$CVSROOT} are reasonable. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Defining the module +@section Defining the module +@cindex Defining a module +@cindex Editing the modules file +@cindex Module, defining +@cindex Modules file, changing + +The next step is to define the module in the +@file{modules} file. This is not strictly necessary, +but modules can be convenient in grouping together +related files and directories. + +In simple cases these steps are sufficient to define a module. + +@enumerate +@item +Get a working copy of the modules file. + +@example +$ cvs checkout CVSROOT/modules +$ cd CVSROOT +@end example + +@item +Edit the file and insert a line that defines the module. @xref{Intro +administrative files}, for an introduction. @xref{modules}, for a full +description of the modules file. You can use the +following line to define the module @samp{tc}: + +@example +tc yoyodyne/tc +@end example + +@item +Commit your changes to the modules file. + +@example +$ cvs commit -m "Added the tc module." modules +@end example + +@item +Release the modules module. + +@example +$ cd .. +$ cvs release -d CVSROOT +@end example +@end enumerate + +@c --------------------------------------------------------------------- +@node Revisions +@chapter Revisions + +For many uses of @sc{cvs}, one doesn't need to worry +too much about revision numbers; @sc{cvs} assigns +numbers such as @code{1.1}, @code{1.2}, and so on, and +that is all one needs to know. However, some people +prefer to have more knowledge and control concerning +how @sc{cvs} assigns revision numbers. + +If one wants to keep track of a set of revisions +involving more than one file, such as which revisions +went into a particular release, one uses a @dfn{tag}, +which is a symbolic revision which can be assigned to a +numeric revision in each file. + +@menu +* Revision numbers:: The meaning of a revision number +* Versions revisions releases:: Terminology used in this manual +* Assigning revisions:: Assigning revisions +* Tags:: Tags--Symbolic revisions +* Tagging the working directory:: The cvs tag command +* Tagging by date/tag:: The cvs rtag command +* Modifying tags:: Adding, renaming, and deleting tags +* Tagging add/remove:: Tags with adding and removing files +* Sticky tags:: Certain tags are persistent +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Revision numbers +@section Revision numbers +@cindex Revision numbers +@cindex Revision tree +@cindex Linear development +@cindex Number, revision- +@cindex Decimal revision number +@cindex Branch number +@cindex Number, branch + +Each version of a file has a unique @dfn{revision +number}. Revision numbers look like @samp{1.1}, +@samp{1.2}, @samp{1.3.2.2} or even @samp{1.3.2.2.4.5}. +A revision number always has an even number of +period-separated decimal integers. By default revision +1.1 is the first revision of a file. Each successive +revision is given a new number by increasing the +rightmost number by one. The following figure displays +a few revisions, with newer revisions to the right. + +@example + +-----+ +-----+ +-----+ +-----+ +-----+ + ! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! + +-----+ +-----+ +-----+ +-----+ +-----+ +@end example + +It is also possible to end up with numbers containing +more than one period, for example @samp{1.3.2.2}. Such +revisions represent revisions on branches +(@pxref{Branching and merging}); such revision numbers +are explained in detail in @ref{Branches and +revisions}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Versions revisions releases +@section Versions, revisions and releases +@cindex Revisions, versions and releases +@cindex Versions, revisions and releases +@cindex Releases, revisions and versions + +A file can have several versions, as described above. +Likewise, a software product can have several versions. +A software product is often given a version number such +as @samp{4.1.1}. + +Versions in the first sense are called @dfn{revisions} +in this document, and versions in the second sense are +called @dfn{releases}. To avoid confusion, the word +@dfn{version} is almost never used in this document. + +@node Assigning revisions +@section Assigning revisions + +@c We avoid the "major revision" terminology. It seems +@c like jargon. Hopefully "first number" is clear enough. +By default, @sc{cvs} will assign numeric revisions by +leaving the first number the same and incrementing the +second number. For example, @code{1.1}, @code{1.2}, +@code{1.3}, etc. + +When adding a new file, the second number will always +be one and the first number will equal the highest +first number of any file in that directory. For +example, the current directory contains files whose +highest numbered revisions are @code{1.7}, @code{3.1}, +and @code{4.12}, then an added file will be given the +numeric revision @code{4.1}. + +@c This is sort of redundant with something we said a +@c while ago. Somewhere we need a better way of +@c introducing how the first number can be anything +@c except "1", perhaps. Also I don't think this +@c presentation is clear on why we are discussing releases +@c and first numbers of numeric revisions in the same +@c breath. +Normally there is no reason to care +about the revision numbers---it is easier to treat them +as internal numbers that @sc{cvs} maintains, and tags +provide a better way to distinguish between things like +release 1 versus release 2 of your product +(@pxref{Tags}). However, if you want to set the +numeric revisions, the @samp{-r} option to @code{cvs +commit} can do that. The @samp{-r} option implies the +@samp{-f} option, in the sense that it causes the +files to be committed even if they are not modified. + +For example, to bring all your files up to +revision 3.0 (including those that haven't changed), +you might invoke: + +@example +$ cvs commit -r 3.0 +@end example + +Note that the number you specify with @samp{-r} must be +larger than any existing revision number. That is, if +revision 3.0 exists, you cannot @samp{cvs commit +-r 1.3}. If you want to maintain several releases in +parallel, you need to use a branch (@pxref{Branching and merging}). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Tags +@section Tags--Symbolic revisions +@cindex Tags + +The revision numbers live a life of their own. They +need not have anything at all to do with the release +numbers of your software product. Depending +on how you use @sc{cvs} the revision numbers might change several times +between two releases. As an example, some of the +source files that make up @sc{rcs} 5.6 have the following +revision numbers: +@cindex RCS revision numbers + +@example +ci.c 5.21 +co.c 5.9 +ident.c 5.3 +rcs.c 5.12 +rcsbase.h 5.11 +rcsdiff.c 5.10 +rcsedit.c 5.11 +rcsfcmp.c 5.9 +rcsgen.c 5.10 +rcslex.c 5.11 +rcsmap.c 5.2 +rcsutil.c 5.10 +@end example + +@cindex tag, command, introduction +@cindex Tag, symbolic name +@cindex Symbolic name (tag) +@cindex Name, symbolic (tag) +@cindex HEAD, as reserved tag name +@cindex BASE, as reserved tag name +You can use the @code{tag} command to give a symbolic name to a +certain revision of a file. You can use the @samp{-v} flag to the +@code{status} command to see all tags that a file has, and +which revision numbers they represent. Tag names must +start with an uppercase or lowercase letter and can +contain uppercase and lowercase letters, digits, +@samp{-}, and @samp{_}. The two tag names @code{BASE} +and @code{HEAD} are reserved for use by @sc{cvs}. It +is expected that future names which are special to +@sc{cvs} will be specially named, for example by +starting with @samp{.}, rather than being named analogously to +@code{BASE} and @code{HEAD}, to avoid conflicts with +actual tag names. +@c Including a character such as % or = has also been +@c suggested as the naming convention for future +@c special tag names. Starting with . is nice because +@c that is not a legal tag name as far as RCS is concerned. +@c FIXME: CVS actually accepts quite a few characters +@c in tag names, not just the ones documented above +@c (see RCS_check_tag). RCS +@c defines legitimate tag names by listing illegal +@c characters rather than legal ones. CVS is said to lose its +@c mind if you try to use "/" (try making such a tag sticky +@c and using "cvs status" client/server--see remote +@c protocol format for entries line for probable cause). +@c TODO: The testsuite +@c should test for whatever are documented above as +@c officially-OK tag names, and CVS should at least reject +@c characters that won't work, like "/". + +You'll want to choose some convention for naming tags, +based on information such as the name of the program +and the version number of the release. For example, +one might take the name of the program, immediately +followed by the version number with @samp{.} changed to +@samp{-}, so that CVS 1.9 would be tagged with the name +@code{cvs1-9}. If you choose a consistent convention, +then you won't constantly be guessing whether a tag is +@code{cvs-1-9} or @code{cvs1_9} or what. You might +even want to consider enforcing your convention in the +taginfo file (@pxref{user-defined logging}). +@c Might be nice to say more about using taginfo this +@c way, like giving an example, or pointing out any particular +@c issues which arise. + +@cindex Adding a tag +@cindex Tag, example +The following example shows how you can add a tag to a +file. The commands must be issued inside your working +directory. That is, you should issue the +command in the directory where @file{backend.c} +resides. + +@example +$ cvs tag rel-0-4 backend.c +T backend.c +$ cvs status -v backend.c +=================================================================== +File: backend.c Status: Up-to-date + + Version: 1.4 Tue Dec 1 14:39:01 1992 + RCS Version: 1.4 /u/cvsroot/yoyodyne/tc/backend.c,v + Sticky Tag: (none) + Sticky Date: (none) + Sticky Options: (none) + + Existing Tags: + rel-0-4 (revision: 1.4) + +@end example + +For a complete summary of the syntax of @code{cvs tag}, +including the various options, see @ref{Invoking CVS}. + +There is seldom reason to tag a file in isolation. A more common use is +to tag all the files that constitute a module with the same tag at +strategic points in the development life-cycle, such as when a release +is made. + +@example +$ cvs tag rel-1-0 . +cvs tag: Tagging . +T Makefile +T backend.c +T driver.c +T frontend.c +T parser.c +@end example + +(When you give @sc{cvs} a directory as argument, it generally applies the +operation to all the files in that directory, and (recursively), to any +subdirectories that it may contain. @xref{Recursive behavior}.) + +@cindex Retrieving an old revision using tags +@cindex Tag, retrieving old revisions +The @code{checkout} command has a flag, @samp{-r}, that lets you check out +a certain revision of a module. This flag makes it easy to +retrieve the sources that make up release 1.0 of the module @samp{tc} at +any time in the future: + +@example +$ cvs checkout -r rel-1-0 tc +@end example + +@noindent +This is useful, for instance, if someone claims that there is a bug in +that release, but you cannot find the bug in the current working copy. + +You can also check out a module as it was at any given date. +@xref{checkout options}. When specifying @samp{-r} to +any of these commands, you will need beware of sticky +tags; see @ref{Sticky tags}. + +When you tag more than one file with the same tag you +can think about the tag as "a curve drawn through a +matrix of filename vs. revision number." Say we have 5 +files with the following revisions: + +@example +@group + file1 file2 file3 file4 file5 + + 1.1 1.1 1.1 1.1 /--1.1* <-*- TAG + 1.2*- 1.2 1.2 -1.2*- + 1.3 \- 1.3*- 1.3 / 1.3 + 1.4 \ 1.4 / 1.4 + \-1.5*- 1.5 + 1.6 +@end group +@end example + +At some time in the past, the @code{*} versions were tagged. +You can think of the tag as a handle attached to the curve +drawn through the tagged revisions. When you pull on +the handle, you get all the tagged revisions. Another +way to look at it is that you "sight" through a set of +revisions that is "flat" along the tagged revisions, +like this: + +@example +@group + file1 file2 file3 file4 file5 + + 1.1 + 1.2 + 1.1 1.3 _ + 1.1 1.2 1.4 1.1 / + 1.2*----1.3*----1.5*----1.2*----1.1 (--- <--- Look here + 1.3 1.6 1.3 \_ + 1.4 1.4 + 1.5 +@end group +@end example + +@node Tagging the working directory +@section Specifying what to tag from the working directory + +@cindex tag (subcommand) +The example in the previous section demonstrates one of +the most common ways to choose which revisions to tag. +Namely, running the @code{cvs tag} command without +arguments causes @sc{cvs} to select the revisions which +are checked out in the current working directory. For +example, if the copy of @file{backend.c} in working +directory was checked out from revision 1.4, then +@sc{cvs} will tag revision 1.4. Note that the tag is +applied immediately to revision 1.4 in the repository; +tagging is not like modifying a file, or other +operations in which one first modifies the working +directory and then runs @code{cvs commit} to transfer +that modification to the repository. + +One potentially surprising aspect of the fact that +@code{cvs tag} operates on the repository is that you +are tagging the checked-in revisions, which may differ +from locally modified files in your working directory. +If you want to avoid doing this by mistake, specify the +@samp{-c} option to @code{cvs tag}. If there are any +locally modified files, @sc{cvs} will abort with an +error before it tags any files: + +@example +$ cvs tag -c rel-0-4 +cvs tag: backend.c is locally modified +cvs [tag aborted]: correct the above errors first! +@end example + +@node Tagging by date/tag +@section Specifying what to tag by date or revision +@cindex rtag (subcommand) + +The @code{cvs rtag} command tags the repository as of a +certain date or time (or can be used to tag the latest +revision). @code{rtag} works directly on the +repository contents (it requires no prior checkout and +does not look for a working directory). + +The following options specify which date or revision to +tag. See @ref{Common options}, for a complete +description of them. + +@table @code +@item -D @var{date} +Tag the most recent revision no later than @var{date}. + +@item -f +Only useful with the @samp{-D @var{date}} or @samp{-r @var{tag}} +flags. If no matching revision is found, use the most +recent revision (instead of ignoring the file). + +@item -r @var{tag} +Only tag those files that contain existing tag @var{tag}. +@end table + +The @code{cvs tag} command also allows one to specify +files by revision or date, using the same @samp{-r}, +@samp{-D}, and @samp{-f} options. However, this +feature is probably not what you want. The reason is +that @code{cvs tag} chooses which files to tag based on +the files that exist in the working directory, rather +than the files which existed as of the given tag/date. +Therefore, you are generally better off using @code{cvs +rtag}. The exceptions might be cases like: + +@example +cvs tag -r 1.4 backend.c +@end example + +@node Modifying tags +@section Deleting, moving, and renaming tags + +@c Also see: +@c "How do I move or rename a magic branch tag?" +@c in the FAQ (I think the issues it talks about still +@c apply, but this could use some sanity.sh work). + +Normally one does not modify tags. They exist in order +to record the history of the repository and so deleting +them or changing their meaning would, generally, not be +what you want. + +However, there might be cases in which one uses a tag +temporarily or accidentally puts one in the wrong +place. Therefore, one might delete, move, or rename a +tag. Warning: the commands in this section are +dangerous; they permanently discard historical +information and it can difficult or impossible to +recover from errors. If you are a @sc{cvs} +administrator, you may consider restricting these +commands with taginfo (@pxref{user-defined logging}). + +@cindex Deleting tags +@cindex Removing tags +@cindex Tags, deleting +To delete a tag, specify the @samp{-d} option to either +@code{cvs tag} or @code{cvs rtag}. For example: + +@example +cvs rtag -d rel-0-4 tc +@end example + +deletes the tag @code{rel-0-4} from the module @code{tc}. + +@cindex Moving tags +@cindex Tags, moving +When we say @dfn{move} a tag, we mean to make the same +name point to different revisions. For example, the +@code{stable} tag may currently point to revision 1.4 +of @file{backend.c} and perhaps we want to make it +point to revision 1.6. To move a tag, specify the +@samp{-F} option to either @code{cvs tag} or @code{cvs +rtag}. For example, the task just mentioned might be +accomplished as: + +@example +cvs tag -r 1.6 -F stable backend.c +@end example + +@cindex Renaming tags +@cindex Tags, renaming +When we say @dfn{rename} a tag, we mean to make a +different name point to the same revisions as the old +tag. For example, one may have misspelled the tag name +and want to correct it (hopefully before others are +relying on the old spelling). To rename a tag, first +create a new tag using the @samp{-r} option to +@code{cvs rtag}, and then delete the old name. This +leaves the new tag on exactly the same files as the old +tag. For example: + +@example +cvs rtag -r old-name-0-4 rel-0-4 tc +cvs rtag -d old-name-0-4 tc +@end example + +@node Tagging add/remove +@section Tagging and adding and removing files + +The subject of exactly how tagging interacts with +adding and removing files is somewhat obscure; for the +most part @sc{cvs} will keep track of whether files +exist or not without too much fussing. By default, +tags are applied to only files which have a revision +corresponding to what is being tagged. Files which did +not exist yet, or which were already removed, simply +omit the tag, and @sc{cvs} knows to treat the absence +of a tag as meaning that the file didn't exist as of +that tag. + +However, this can lose a small amount of information. +For example, suppose a file was added and then removed. +Then, if the tag is missing for that file, there is no +way to know whether the tag refers to the time before +the file was added, or the time after it was removed. +If you specify the @samp{-r} option to @code{cvs rtag}, +then @sc{cvs} tags the files which have been removed, +and thereby avoids this problem. For example, one +might specify @code{-r HEAD} to tag the head. + +On the subject of adding and removing files, the +@code{cvs rtag} command has a @samp{-a} option which +means to clear the tag from removed files that would +not otherwise be tagged. For example, one might +specify this option in conjunction with @samp{-F} when +moving a tag. If one moved a tag without @samp{-a}, +then the tag in the removed files might still refer to +the old revision, rather than reflecting the fact that +the file had been removed. I don't think this is +necessary if @samp{-r} is specified, as noted above. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Sticky tags +@section Sticky tags +@cindex Sticky tags +@cindex Tags, sticky + +@c A somewhat related issue is per-directory sticky +@c tags (see comment at CVS/Tag in node Working +@c directory storage); we probably want to say +@c something like "you can set a sticky tag for only +@c some files, but you don't want to" or some such. + +Sometimes a working copy's revision has extra data +associated with it, for example it might be on a branch +(@pxref{Branching and merging}), or restricted to +versions prior to a certain date by @samp{checkout -D} +or @samp{update -D}. Because this data persists -- +that is, it applies to subsequent commands in the +working copy -- we refer to it as @dfn{sticky}. + +Most of the time, stickiness is an obscure aspect of +@sc{cvs} that you don't need to think about. However, +even if you don't want to use the feature, you may need +to know @emph{something} about sticky tags (for +example, how to avoid them!). + +You can use the @code{status} command to see if any +sticky tags or dates are set: + +@example +$ cvs status driver.c +=================================================================== +File: driver.c Status: Up-to-date + + Version: 1.7.2.1 Sat Dec 5 19:35:03 1992 + RCS Version: 1.7.2.1 /u/cvsroot/yoyodyne/tc/driver.c,v + Sticky Tag: rel-1-0-patches (branch: 1.7.2) + Sticky Date: (none) + Sticky Options: (none) + +@end example + +@cindex Resetting sticky tags +@cindex Sticky tags, resetting +@cindex Deleting sticky tags +The sticky tags will remain on your working files until +you delete them with @samp{cvs update -A}. The +@samp{-A} option retrieves the version of the file from +the head of the trunk, and forgets any sticky tags, +dates, or options. + +@cindex Sticky date +The most common use of sticky tags is to identify which +branch one is working on, as described in +@ref{Accessing branches}. However, non-branch +sticky tags have uses as well. For example, +suppose that you want to avoid updating your working +directory, to isolate yourself from possibly +destabilizing changes other people are making. You +can, of course, just refrain from running @code{cvs +update}. But if you want to avoid updating only a +portion of a larger tree, then sticky tags can help. +If you check out a certain revision (such as 1.4) it +will become sticky. Subsequent @code{cvs update} +commands will +not retrieve the latest revision until you reset the +tag with @code{cvs update -A}. Likewise, use of the +@samp{-D} option to @code{update} or @code{checkout} +sets a @dfn{sticky date}, which, similarly, causes that +date to be used for future retrievals. + +People often want to retrieve an old version of +a file without setting a sticky tag. This can +be done with the @samp{-p} option to @code{checkout} or +@code{update}, which sends the contents of the file to +standard output. For example: +@example +$ cvs update -p -r 1.1 file1 >file1 +=================================================================== +Checking out file1 +RCS: /tmp/cvs-sanity/cvsroot/first-dir/Attic/file1,v +VERS: 1.1 +*************** +$ +@end example + +However, this isn't the easiest way, if you are asking +how to undo a previous checkin (in this example, put +@file{file1} back to the way it was as of revision +1.1). In that case you are better off using the +@samp{-j} option to @code{update}; for further +discussion see @ref{Merging two revisions}. + +@c --------------------------------------------------------------------- +@node Branching and merging +@chapter Branching and merging +@cindex Branching +@cindex Merging +@cindex Copying changes +@cindex Main trunk and branches +@cindex Revision tree, making branches +@cindex Branches, copying changes between +@cindex Changes, copying between branches +@cindex Modifications, copying between branches + +CVS allows you to isolate changes onto a separate +line of development, known as a @dfn{branch}. When you +change files on a branch, those changes do not appear +on the main trunk or other branches. + +Later you can move changes from one branch to another +branch (or the main trunk) by @dfn{merging}. Merging +involves first running @code{cvs update -j}, to merge +the changes into the working directory. +You can then commit that revision, and thus effectively +copy the changes onto another branch. + +@menu +* Branches motivation:: What branches are good for +* Creating a branch:: Creating a branch +* Accessing branches:: Checking out and updating branches +* Branches and revisions:: Branches are reflected in revision numbers +* Magic branch numbers:: Magic branch numbers +* Merging a branch:: Merging an entire branch +* Merging more than once:: Merging from a branch several times +* Merging two revisions:: Merging differences between two revisions +* Merging adds and removals:: What if files are added or removed? +* Merging and keywords:: Avoiding conflicts due to keyword substitution +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Branches motivation +@section What branches are good for +@cindex Branches motivation +@cindex What branches are good for +@cindex Motivation for branches + +@c FIXME: this node mentions one way to use branches, +@c but it is by no means the only way. For example, +@c the technique of committing a new feature on a branch, +@c until it is ready for the main trunk. The whole +@c thing is generally speaking more akin to the +@c "Revision management" node although it isn't clear to +@c me whether policy matters should be centralized or +@c distributed throughout the relevant sections. +Suppose that release 1.0 of tc has been made. You are continuing to +develop tc, planning to create release 1.1 in a couple of months. After a +while your customers start to complain about a fatal bug. You check +out release 1.0 (@pxref{Tags}) and find the bug +(which turns out to have a trivial fix). However, the current revision +of the sources are in a state of flux and are not expected to be stable +for at least another month. There is no way to make a +bugfix release based on the newest sources. + +The thing to do in a situation like this is to create a @dfn{branch} on +the revision trees for all the files that make up +release 1.0 of tc. You can then make +modifications to the branch without disturbing the main trunk. When the +modifications are finished you can elect to either incorporate them on +the main trunk, or leave them on the branch. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Creating a branch +@section Creating a branch +@cindex Creating a branch +@cindex Branch, creating a +@cindex tag, creating a branch using +@cindex rtag, creating a branch using + +You can create a branch with @code{tag -b}; for +example, assuming you're in a working copy: + +@example +$ cvs tag -b rel-1-0-patches +@end example + +@c FIXME: we should be more explicit about the value of +@c having a tag on the branchpoint. For example +@c "cvs tag rel-1-0-patches-branchpoint" before +@c the "cvs tag -b". This points out that +@c rel-1-0-patches is a pretty awkward name for +@c this example (more so than for the rtag example +@c below). + +This splits off a branch based on the current revisions +in the working copy, assigning that branch the name +@samp{rel-1-0-patches}. + +It is important to understand that branches get created +in the repository, not in the working copy. Creating a +branch based on current revisions, as the above example +does, will @emph{not} automatically switch the working +copy to be on the new branch. For information on how +to do that, see @ref{Accessing branches}. + +You can also create a branch without reference to any +working copy, by using @code{rtag}: + +@example +$ cvs rtag -b -r rel-1-0 rel-1-0-patches tc +@end example + +@samp{-r rel-1-0} says that this branch should be +rooted at the revision that +corresponds to the tag @samp{rel-1-0}. It need not +be the most recent revision -- it's often useful to +split a branch off an old revision (for example, when +fixing a bug in a past release otherwise known to be +stable). + +As with @samp{tag}, the @samp{-b} flag tells +@code{rtag} to create a branch (rather than just a +symbolic revision name). Note that the numeric +revision number that matches @samp{rel-1-0} will +probably be different from file to file. + +So, the full effect of the command is to create a new +branch -- named @samp{rel-1-0-patches} -- in module +@samp{tc}, rooted in the revision tree at the point tagged +by @samp{rel-1-0}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Accessing branches +@section Accessing branches +@cindex Check out a branch +@cindex Retrieve a branch +@cindex Access a branch +@cindex Identifying a branch +@cindex Branch, check out +@cindex Branch, retrieving +@cindex Branch, accessing +@cindex Branch, identifying + +You can retrieve a branch in one of two ways: by +checking it out fresh from the repository, or by +switching an existing working copy over to the branch. + +To check out a branch from the repository, invoke +@samp{checkout} with the @samp{-r} flag, followed by +the tag name of the branch (@pxref{Creating a branch}): + +@example +$ cvs checkout -r rel-1-0-patches tc +@end example + +Or, if you already have a working copy, you can switch +it to a given branch with @samp{update -r}: + +@example +$ cvs update -r rel-1-0-patches tc +@end example + +or equivalently: + +@example +$ cd tc +$ cvs update -r rel-1-0-patches +@end example + +It does not matter if the working copy was originally +on the main trunk or on some other branch -- the above +command will switch it to the named branch. And +similarly to a regular @samp{update} command, +@samp{update -r} merges any changes you have made, +notifying you of conflicts where they occur. + +Once you have a working copy tied to a particular +branch, it remains there until you tell it otherwise. +This means that changes checked in from the working +copy will add new revisions on that branch, while +leaving the main trunk and other branches unaffected. + +@cindex Branches, sticky +To find out what branch a working copy is on, you can +use the @samp{status} command. In its output, look for +the field named @samp{Sticky tag} (@pxref{Sticky tags}) +-- that's @sc{cvs}'s way of telling you the branch, if +any, of the current working files: + +@example +$ cvs status -v driver.c backend.c +=================================================================== +File: driver.c Status: Up-to-date + + Version: 1.7 Sat Dec 5 18:25:54 1992 + RCS Version: 1.7 /u/cvsroot/yoyodyne/tc/driver.c,v + Sticky Tag: rel-1-0-patches (branch: 1.7.2) + Sticky Date: (none) + Sticky Options: (none) + + Existing Tags: + rel-1-0-patches (branch: 1.7.2) + rel-1-0 (revision: 1.7) + +=================================================================== +File: backend.c Status: Up-to-date + + Version: 1.4 Tue Dec 1 14:39:01 1992 + RCS Version: 1.4 /u/cvsroot/yoyodyne/tc/backend.c,v + Sticky Tag: rel-1-0-patches (branch: 1.4.2) + Sticky Date: (none) + Sticky Options: (none) + + Existing Tags: + rel-1-0-patches (branch: 1.4.2) + rel-1-0 (revision: 1.4) + rel-0-4 (revision: 1.4) + +@end example + +Don't be confused by the fact that the branch numbers +for each file are different (@samp{1.7.2} and +@samp{1.4.2} respectively). The branch tag is the +same, @samp{rel-1-0-patches}, and the files are +indeed on the same branch. The numbers simply reflect +the point in each file's revision history at which the +branch was made. In the above example, one can deduce +that @samp{driver.c} had been through more changes than +@samp{backend.c} before this branch was created. + +See @ref{Branches and revisions} for details about how +branch numbers are constructed. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Branches and revisions +@section Branches and revisions +@cindex Branch number +@cindex Number, branch +@cindex Revision numbers (branches) + +Ordinarily, a file's revision history is a linear +series of increments (@pxref{Revision numbers}): + +@example + +-----+ +-----+ +-----+ +-----+ +-----+ + ! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! + +-----+ +-----+ +-----+ +-----+ +-----+ +@end example + +However, @sc{cvs} is not limited to linear development. The +@dfn{revision tree} can be split into @dfn{branches}, +where each branch is a self-maintained line of +development. Changes made on one branch can easily be +moved back to the main trunk. + +Each branch has a @dfn{branch number}, consisting of an +odd number of period-separated decimal integers. The +branch number is created by appending an integer to the +revision number where the corresponding branch forked +off. Having branch numbers allows more than one branch +to be forked off from a certain revision. + +@need 3500 +All revisions on a branch have revision numbers formed +by appending an ordinal number to the branch number. +The following figure illustrates branching with an +example. + +@example +@c This example used to have a 1.2.2.4 revision, which +@c might help clarify that development can continue on +@c 1.2.2. Might be worth reinstating if it can be done +@c without overfull hboxes. +@group + +-------------+ + Branch 1.2.2.3.2 -> ! 1.2.2.3.2.1 ! + / +-------------+ + / + / + +---------+ +---------+ +---------+ +Branch 1.2.2 -> _! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.3 ! + / +---------+ +---------+ +---------+ + / + / ++-----+ +-----+ +-----+ +-----+ +-----+ +! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- The main trunk ++-----+ +-----+ +-----+ +-----+ +-----+ + ! + ! + ! +---------+ +---------+ +---------+ +Branch 1.2.4 -> +---! 1.2.4.1 !----! 1.2.4.2 !----! 1.2.4.3 ! + +---------+ +---------+ +---------+ + +@end group +@end example + +@c -- However, at least for me the figure is not enough. I suggest more +@c -- text to accompany it. "A picture is worth a thousand words", so you +@c -- have to make sure the reader notices the couple of hundred words +@c -- *you* had in mind more than the others! + +@c -- Why an even number of segments? This section implies that this is +@c -- how the main trunk is distinguished from branch roots, but you never +@c -- explicitly say that this is the purpose of the [by itself rather +@c -- surprising] restriction to an even number of segments. + +The exact details of how the branch number is +constructed is not something you normally need to be +concerned about, but here is how it works: When +@sc{cvs} creates a branch number it picks the first +unused even integer, starting with 2. So when you want +to create a branch from revision 6.4 it will be +numbered 6.4.2. All branch numbers ending in a zero +(such as 6.4.0) are used internally by @sc{cvs} +(@pxref{Magic branch numbers}). The branch 1.1.1 has a +special meaning. @xref{Tracking sources}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Magic branch numbers +@section Magic branch numbers + +@c Want xref to here from "log"? + +This section describes a @sc{cvs} feature called +@dfn{magic branches}. For most purposes, you need not +worry about magic branches; @sc{cvs} handles them for +you. However, they are visible to you in certain +circumstances, so it may be useful to have some idea of +how it works. + +Externally, branch numbers consist of an odd number of +dot-separated decimal integers. @xref{Revision +numbers}. That is not the whole truth, however. For +efficiency reasons @sc{cvs} sometimes inserts an extra 0 +in the second rightmost position (1.2.4 becomes +1.2.0.4, 8.9.10.11.12 becomes 8.9.10.11.0.12 and so +on). + +@sc{cvs} does a pretty good job at hiding these so +called magic branches, but in a few places the hiding +is incomplete: + +@itemize @bullet +@ignore +@c This is in ignore as I'm taking their word for it, +@c that this was fixed +@c a long time ago. But before deleting this +@c entirely, I'd rather verify it (and add a test +@c case to the testsuite). +@item +The magic branch can appear in the output from +@code{cvs status} in vanilla @sc{cvs} 1.3. This is +fixed in @sc{cvs} 1.3-s2. + +@end ignore +@item +The magic branch number appears in the output from +@code{cvs log}. +@c What output should appear instead? + +@item +You cannot specify a symbolic branch name to @code{cvs +admin}. + +@end itemize + +@c Can CVS do this automatically the first time +@c you check something in to that branch? Should +@c it? +You can use the @code{admin} command to reassign a +symbolic name to a branch the way @sc{rcs} expects it +to be. If @code{R4patches} is assigned to the branch +1.4.2 (magic branch number 1.4.0.2) in file +@file{numbers.c} you can do this: + +@example +$ cvs admin -NR4patches:1.4.2 numbers.c +@end example + +It only works if at least one revision is already +committed on the branch. Be very careful so that you +do not assign the tag to the wrong number. (There is +no way to see how the tag was assigned yesterday). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Merging a branch +@section Merging an entire branch +@cindex Merging a branch +@cindex -j (merging branches) + +You can merge changes made on a branch into your working copy by giving +the @samp{-j @var{branch}} flag to the @code{update} command. With one +@samp{-j @var{branch}} option it merges the changes made between the +point where the branch forked and newest revision on that branch (into +your working copy). + +@cindex Join +The @samp{-j} stands for ``join''. + +@cindex Branch merge example +@cindex Example, branch merge +@cindex Merge, branch example +Consider this revision tree: + +@example ++-----+ +-----+ +-----+ +-----+ +! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 ! <- The main trunk ++-----+ +-----+ +-----+ +-----+ + ! + ! + ! +---------+ +---------+ +Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 ! + +---------+ +---------+ +@end example + +@noindent +The branch 1.2.2 has been given the tag (symbolic name) @samp{R1fix}. The +following example assumes that the module @samp{mod} contains only one +file, @file{m.c}. + +@example +$ cvs checkout mod # @r{Retrieve the latest revision, 1.4} + +$ cvs update -j R1fix m.c # @r{Merge all changes made on the branch,} + # @r{i.e. the changes between revision 1.2} + # @r{and 1.2.2.2, into your working copy} + # @r{of the file.} + +$ cvs commit -m "Included R1fix" # @r{Create revision 1.5.} +@end example + +A conflict can result from a merge operation. If that +happens, you should resolve it before committing the +new revision. @xref{Conflicts example}. + +If your source files contain keywords (@pxref{Keyword substitution}), +you might be getting more conflicts than strictly necessary. See +@ref{Merging and keywords}, for information on how to avoid this. + +The @code{checkout} command also supports the @samp{-j @var{branch}} flag. The +same effect as above could be achieved with this: + +@example +$ cvs checkout -j R1fix mod +$ cvs commit -m "Included R1fix" +@end example + +@node Merging more than once +@section Merging from a branch several times + +Continuing our example, the revision tree now looks +like this: + +@example ++-----+ +-----+ +-----+ +-----+ +-----+ +! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- The main trunk ++-----+ +-----+ +-----+ +-----+ +-----+ + ! * + ! * + ! +---------+ +---------+ +Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 ! + +---------+ +---------+ +@end example + +where the starred line represents the merge from the +@samp{R1fix} branch to the main trunk, as just +discussed. + +Now suppose that development continues on the +@samp{R1fix} branch: + +@example ++-----+ +-----+ +-----+ +-----+ +-----+ +! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- The main trunk ++-----+ +-----+ +-----+ +-----+ +-----+ + ! * + ! * + ! +---------+ +---------+ +---------+ +Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.3 ! + +---------+ +---------+ +---------+ +@end example + +and then you want to merge those new changes onto the +main trunk. If you just use the @code{cvs update -j +R1fix m.c} command again, @sc{cvs} will attempt to +merge again the changes which you have already merged, +which can have undesirable side effects. + +So instead you need to specify that you only want to +merge the changes on the branch which have not yet been +merged into the trunk. To do that you specify two +@samp{-j} options, and @sc{cvs} merges the changes from +the first revision to the second revision. For +example, in this case the simplest way would be + +@example +cvs update -j 1.2.2.2 -j R1fix m.c # @r{Merge changes from 1.2.2.2 to the} + # @r{head of the R1fix branch} +@end example + +The problem with this is that you need to specify the +1.2.2.2 revision manually. A slightly better approach +might be to use the date the last merge was done: + +@example +cvs update -j R1fix:yesterday -j R1fix m.c +@end example + +Better yet, tag the R1fix branch after every merge into +the trunk, and then use that tag for subsequent merges: + +@example +cvs update -j merged_from_R1fix_to_trunk -j R1fix m.c +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Merging two revisions +@section Merging differences between any two revisions +@cindex Merging two revisions +@cindex Revisions, merging differences between +@cindex Differences, merging + +With two @samp{-j @var{revision}} flags, the @code{update} +(and @code{checkout}) command can merge the differences +between any two revisions into your working file. + +@cindex Undoing a change +@cindex Removing a change +@example +$ cvs update -j 1.5 -j 1.3 backend.c +@end example + +@noindent +will undo all changes made between revision +1.3 and 1.5. Note the order of the revisions! + +If you try to use this option when operating on +multiple files, remember that the numeric revisions will +probably be very different between the various files. +You almost always use symbolic +tags rather than revision numbers when operating on +multiple files. + +@cindex Restoring old version of removed file +@cindex Resurrecting old version of dead file +Specifying two @samp{-j} options can also undo file +removals or additions. For example, suppose you have +a file +named @file{file1} which existed as revision 1.1, and +you then removed it (thus adding a dead revision 1.2). +Now suppose you want to add it again, with the same +contents it had previously. Here is how to do it: + +@example +$ cvs update -j 1.2 -j 1.1 file1 +U file1 +$ cvs commit -m test +Checking in file1; +/tmp/cvs-sanity/cvsroot/first-dir/file1,v <-- file1 +new revision: 1.3; previous revision: 1.2 +done +$ +@end example + +@node Merging adds and removals +@section Merging can add or remove files + +If the changes which you are merging involve removing +or adding some files, @code{update -j} will reflect +such additions or removals. + +@c FIXME: This example needs a lot more explanation. +@c We also need other examples for some of the other +@c cases (not all--there are too many--as long as we present a +@c coherent general principle). +For example: +@example +cvs update -A +touch a b c +cvs add a b c ; cvs ci -m "added" a b c +cvs tag -b branchtag +cvs update -r branchtag +touch d ; cvs add d +rm a ; cvs rm a +cvs ci -m "added d, removed a" +cvs update -A +cvs update -jbranchtag +@end example + +After these commands are executed and a @samp{cvs commit} is done, +file @file{a} will be removed and file @file{d} added in the main branch. +@c (which was determined by trying it) + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Merging and keywords +@section Merging and keywords +@cindex Merging, and keyword substitution +@cindex Keyword substitution, and merging +@cindex -j (merging branches), and keyword substitution +@cindex -kk, to avoid conflicts during a merge + +If you merge files containing keywords (@pxref{Keyword +substitution}), you will normally get numerous +conflicts during the merge, because the keywords are +expanded differently in the revisions which you are +merging. + +Therefore, you will often want to specify the +@samp{-kk} (@pxref{Substitution modes}) switch to the +merge command line. By substituting just the name of +the keyword, not the expanded value of that keyword, +this option ensures that the revisions which you are +merging will be the same as each other, and avoid +spurious conflicts. + +For example, suppose you have a file like this: + +@example + +---------+ + _! 1.1.2.1 ! <- br1 + / +---------+ + / + / ++-----+ +-----+ +! 1.1 !----! 1.2 ! ++-----+ +-----+ +@end example + +and your working directory is currently on the trunk +(revision 1.2). Then you might get the following +results from a merge: + +@example +$ cat file1 +key $@asis{}Revision: 1.2 $ +. . . +$ cvs update -j br1 +U file1 +RCS file: /cvsroot/first-dir/file1,v +retrieving revision 1.1 +retrieving revision 1.1.2.1 +Merging differences between 1.1 and 1.1.2.1 into file1 +rcsmerge: warning: conflicts during merge +$ cat file1 +@asis{}<<<<<<< file1 +key $@asis{}Revision: 1.2 $ +@asis{}======= +key $@asis{}Revision: 1.1.2.1 $ +@asis{}>>>>>>> 1.1.2.1 +. . . +@end example + +What happened was that the merge tried to merge the +differences between 1.1 and 1.1.2.1 into your working +directory. So, since the keyword changed from +@code{Revision: 1.1} to @code{Revision: 1.1.2.1}, +@sc{cvs} tried to merge that change into your working +directory, which conflicted with the fact that your +working directory had contained @code{Revision: 1.2}. + +Here is what happens if you had used @samp{-kk}: + +@example +$ cat file1 +key $@asis{}Revision: 1.2 $ +. . . +$ cvs update -kk -j br1 +U file1 +RCS file: /cvsroot/first-dir/file1,v +retrieving revision 1.1 +retrieving revision 1.1.2.1 +Merging differences between 1.1 and 1.1.2.1 into file1 +$ cat file1 +key $@asis{}Revision$ +. . . +@end example + +What is going on here is that revision 1.1 and 1.1.2.1 +both expand as plain @code{Revision}, and therefore +merging the changes between them into the working +directory need not change anything. Therefore, there +is no conflict. + +There is, however, one major caveat with using +@samp{-kk} on merges. Namely, it overrides whatever +keyword expansion mode @sc{cvs} would normally have +used. In particular, this is a problem if the mode had +been @samp{-kb} for a binary file. Therefore, if your +repository contains binary files, you will need to deal +with the conflicts rather than using @samp{-kk}. + +@ignore +The following seems rather confusing, possibly buggy, +and in general, in need of much more thought before it +is a recommended technique. For one thing, does it +apply on Windows as well as on Unix? + +Unchanged binary files will undergo the same keyword substitution +but will not be checked in on a subsequent +@code{cvs commit}. Be aware that binary files containing keyword +strings which are present in or below the working directory +will most likely remain corrupt until repaired, however. A simple +@code{cvs update -A} is sufficient to fix these otherwise unaltered binary files +if the merge is being done to the main branch but +this must be done after the merge has been committed or the merge itself +will be lost. + +For Example: +@example +cvs update -Akk -jbranchtag +cvs commit +cvs update -A +@end example + +@noindent +will update the current directory from the main trunk of the +repository, substituting the base keyword strings for keywords, +and merge changes made on the branch @samp{branchtag} into the new +work files, performing the same keyword substitution on that file set before +comparing the two sets. The final @code{cvs update -A} will restore any +corrupted binary files as well as resetting the sticky @samp{-kk} tags which +were present on the files in and below the working directory. +Unfortunately, this doesn't work as well with an arbitrary branch tag, as the +@samp{-r @var{branchtag}} switch does not reset the sticky @samp{-kk} +switches attached to the working files as @samp{-A} does. The workaround +for this is to release the working directory after the merge has been +committed and check it out again. +@end ignore + +@c --------------------------------------------------------------------- +@node Recursive behavior +@chapter Recursive behavior +@cindex Recursive (directory descending) +@cindex Directory, descending +@cindex Descending directories +@cindex Subdirectories + +Almost all of the subcommands of @sc{cvs} work +recursively when you specify a directory as an +argument. For instance, consider this directory +structure: + +@example + @code{$HOME} + | + +--@t{tc} + | | + +--@t{CVS} + | (internal @sc{cvs} files) + +--@t{Makefile} + +--@t{backend.c} + +--@t{driver.c} + +--@t{frontend.c} + +--@t{parser.c} + +--@t{man} + | | + | +--@t{CVS} + | | (internal @sc{cvs} files) + | +--@t{tc.1} + | + +--@t{testing} + | + +--@t{CVS} + | (internal @sc{cvs} files) + +--@t{testpgm.t} + +--@t{test2.t} +@end example + +@noindent +If @file{tc} is the current working directory, the +following is true: + +@itemize @bullet +@item +@samp{cvs update testing} is equivalent to + +@example +cvs update testing/testpgm.t testing/test2.t +@end example + +@item +@samp{cvs update testing man} updates all files in the +subdirectories + +@item +@samp{cvs update .} or just @samp{cvs update} updates +all files in the @code{tc} directory +@end itemize + +If no arguments are given to @code{update} it will +update all files in the current working directory and +all its subdirectories. In other words, @file{.} is a +default argument to @code{update}. This is also true +for most of the @sc{cvs} subcommands, not only the +@code{update} command. + +The recursive behavior of the @sc{cvs} subcommands can be +turned off with the @samp{-l} option. +Conversely, the @samp{-R} option can be used to force recursion if +@samp{-l} is specified in @file{~/.cvsrc} (@pxref{~/.cvsrc}). + +@example +$ cvs update -l # @r{Don't update files in subdirectories} +@end example + +@c --------------------------------------------------------------------- +@node Adding and removing +@chapter Adding, removing, and renaming files and directories + +In the course of a project, one will often add new +files. Likewise with removing or renaming, or with +directories. The general concept to keep in mind in +all these cases is that instead of making an +irreversible change you want @sc{cvs} to record the +fact that a change has taken place, just as with +modifying an existing file. The exact mechanisms to do +this in @sc{cvs} vary depending on the situation. + +@menu +* Adding files:: Adding files +* Removing files:: Removing files +* Removing directories:: Removing directories +* Moving files:: Moving and renaming files +* Moving directories:: Moving and renaming directories +@end menu + +@node Adding files +@section Adding files to a directory +@cindex Adding files + +To add a new file to a directory, follow these steps. + +@itemize @bullet +@item +You must have a working copy of the directory. +@xref{Getting the source}. + +@item +Create the new file inside your working copy of the directory. + +@item +Use @samp{cvs add @var{filename}} to tell @sc{cvs} that you +want to version control the file. If the file contains +binary data, specify @samp{-kb} (@pxref{Binary files}). + +@item +Use @samp{cvs commit @var{filename}} to actually check +in the file into the repository. Other developers +cannot see the file until you perform this step. +@end itemize + +You can also use the @code{add} command to add a new +directory. +@c FIXCVS and/or FIXME: Adding a directory doesn't +@c require the commit step. This probably can be +@c considered a CVS bug, but it is possible we should +@c warn people since this behavior probably won't be +@c changing right away. + +Unlike most other commands, the @code{add} command is +not recursive. You cannot even type @samp{cvs add +foo/bar}! Instead, you have to +@c FIXCVS: This is, of course, not a feature. It is +@c just that no one has gotten around to fixing "cvs add +@c foo/bar". + +@example +$ cd foo +$ cvs add bar +@end example + +@cindex add (subcommand) +@deffn Command {cvs add} [@code{-k} kflag] [@code{-m} message] files @dots{} + +Schedule @var{files} to be added to the repository. +The files or directories specified with @code{add} must +already exist in the current directory. To add a whole +new directory hierarchy to the source repository (for +example, files received from a third-party vendor), use +the @code{import} command instead. @xref{import}. + +The added files are not placed in the source repository +until you use @code{commit} to make the change +permanent. Doing an @code{add} on a file that was +removed with the @code{remove} command will undo the +effect of the @code{remove}, unless a @code{commit} +command intervened. @xref{Removing files}, for an +example. + +The @samp{-k} option specifies the default way that +this file will be checked out; for more information see +@ref{Substitution modes}. + +@c As noted in BUGS, -m is broken client/server (Nov +@c 96). Also see testsuite log2-* tests. +The @samp{-m} option specifies a description for the +file. This description appears in the history log (if +it is enabled, @pxref{history file}). It will also be +saved in the version history inside the repository when +the file is committed. The @code{log} command displays +this description. The description can be changed using +@samp{admin -t}. @xref{admin}. If you omit the +@samp{-m @var{description}} flag, an empty string will +be used. You will not be prompted for a description. +@end deffn + +For example, the following commands add the file +@file{backend.c} to the repository: + +@c This example used to specify +@c -m "Optimizer and code generation passes." +@c to the cvs add command, but that doesn't work +@c client/server (see log2 in sanity.sh). Should fix CVS, +@c but also seems strange to document things which +@c don't work... +@example +$ cvs add backend.c +$ cvs commit -m "Early version. Not yet compilable." backend.c +@end example + +When you add a file it is added only on the branch +which you are working on (@pxref{Branching and merging}). You can +later merge the additions to another branch if you want +(@pxref{Merging adds and removals}). +@c Should we mention that earlier versions of CVS +@c lacked this feature (1.3) or implemented it in a buggy +@c way (well, 1.8 had many bugs in cvs update -j)? +@c Should we mention the bug/limitation regarding a +@c file being a regular file on one branch and a directory +@c on another? +@c FIXME: This needs an example, or several, here or +@c elsewhere, for it to make much sense. +@c Somewhere we need to discuss the aspects of death +@c support which don't involve branching, I guess. +@c Like the ability to re-create a release from a tag. + +@c --------------------------------------------------------------------- +@node Removing files +@section Removing files +@cindex Removing files +@cindex Deleting files + +@c FIXME: this node wants to be split into several +@c smaller nodes. Could make these children of +@c "Adding and removing", probably (death support could +@c be its own section, for example, as could the +@c various bits about undoing mistakes in adding and +@c removing). +Directories change. New files are added, and old files +disappear. Still, you want to be able to retrieve an +exact copy of old releases. + +Here is what you can do to remove a file, +but remain able to retrieve old revisions: + +@itemize @bullet +@c FIXME: should probably be saying something about +@c having a working directory in the first place. +@item +Make sure that you have not made any uncommitted +modifications to the file. @xref{Viewing differences}, +for one way to do that. You can also use the +@code{status} or @code{update} command. If you remove +the file without committing your changes, you will of +course not be able to retrieve the file as it was +immediately before you deleted it. + +@item +Remove the file from your working copy of the directory. +You can for instance use @code{rm}. + +@item +Use @samp{cvs remove @var{filename}} to tell @sc{cvs} that +you really want to delete the file. + +@item +Use @samp{cvs commit @var{filename}} to actually +perform the removal of the file from the repository. +@end itemize + +@c FIXME: Somehow this should be linked in with a more +@c general discussion of death support. I don't know +@c whether we want to use the term "death support" or +@c not (we can perhaps get by without it), but we do +@c need to discuss the "dead" state in "cvs log" and +@c related subjects. The current discussion is +@c scattered around, and not xref'd to each other. +@c FIXME: I think this paragraph wants to be moved +@c later down, at least after the first example. +When you commit the removal of the file, @sc{cvs} +records the fact that the file no longer exists. It is +possible for a file to exist on only some branches and +not on others, or to re-add another file with the same +name later. CVS will correctly create or not create +the file, based on the @samp{-r} and @samp{-D} options +specified to @code{checkout} or @code{update}. + +@c FIXME: This style seems to clash with how we +@c document things in general. +@cindex Remove (subcommand) +@deffn Command {cvs remove} [options] files @dots{} + +Schedule file(s) to be removed from the repository +(files which have not already been removed from the +working directory are not processed). This command +does not actually remove the file from the repository +until you commit the removal. For a full list of +options, see @ref{Invoking CVS}. +@end deffn + +Here is an example of removing several files: + +@example +$ cd test +$ rm *.c +$ cvs remove +cvs remove: Removing . +cvs remove: scheduling a.c for removal +cvs remove: scheduling b.c for removal +cvs remove: use 'cvs commit' to remove these files permanently +$ cvs ci -m "Removed unneeded files" +cvs commit: Examining . +cvs commit: Committing . +@end example + +As a convenience you can remove the file and @code{cvs +remove} it in one step, by specifying the @samp{-f} +option. For example, the above example could also be +done like this: + +@example +$ cd test +$ cvs remove -f *.c +cvs remove: scheduling a.c for removal +cvs remove: scheduling b.c for removal +cvs remove: use 'cvs commit' to remove these files permanently +$ cvs ci -m "Removed unneeded files" +cvs commit: Examining . +cvs commit: Committing . +@end example + +If you execute @code{remove} for a file, and then +change your mind before you commit, you can undo the +@code{remove} with an @code{add} command. +@ignore +@c is this worth saying or not? Somehow it seems +@c confusing to me. +Of course, +since you have removed your copy of file in the working +directory, @sc{cvs} does not necessarily bring back the +contents of the file from right before you executed +@code{remove}; instead it gets the file from the +repository again. +@end ignore + +@c FIXME: what if you change your mind after you commit +@c it? (answer is also "cvs add" but we don't say that...). +@c We need some index entries for thinks like "undoing +@c removal" too. + +@example +$ ls +CVS ja.h oj.c +$ rm oj.c +$ cvs remove oj.c +cvs remove: scheduling oj.c for removal +cvs remove: use 'cvs commit' to remove this file permanently +$ cvs add oj.c +U oj.c +cvs add: oj.c, version 1.1.1.1, resurrected +@end example + +If you realize your mistake before you run the +@code{remove} command you can use @code{update} to +resurrect the file: + +@example +$ rm oj.c +$ cvs update oj.c +cvs update: warning: oj.c was lost +U oj.c +@end example + +When you remove a file it is removed only on the branch +which you are working on (@pxref{Branching and merging}). You can +later merge the removals to another branch if you want +(@pxref{Merging adds and removals}). + +@node Removing directories +@section Removing directories +@cindex Removing directories +@cindex Directories, removing + +In concept removing directories is somewhat similar to +removing files---you want the directory to not exist in +your current working directories, but you also want to +be able to retrieve old releases in which the directory +existed. + +The way that you remove a directory is to remove all +the files in it. You don't remove the directory +itself; there is no way to do that. +Instead you specify the @samp{-P} option to +@code{cvs update}, @code{cvs checkout}, or @code{cvs +export}, which will cause @sc{cvs} to remove empty +directories from working directories. Probably the +best way to do this is to always specify @samp{-P}; if +you want an empty directory then put a dummy file (for +example @file{.keepme}) in it to prevent @samp{-P} from +removing it. + +@c I'd try to give a rationale for this, but I'm not +@c sure there is a particularly convincing one. What +@c we would _like_ is for CVS to do a better job of version +@c controlling whether directories exist, to eliminate the +@c need for -P and so that a file can be a directory in +@c one revision and a regular file in another. +Note that @samp{-P} is implied by the @samp{-r} or @samp{-D} +options of @code{checkout} and @code{export}. This way +@sc{cvs} will be able to correctly create the directory +or not depending on whether the particular version you +are checking out contains any files in that directory. + +@c --------------------------------------------------------------------- +@node Moving files +@section Moving and renaming files +@cindex Moving files +@cindex Renaming files +@cindex Files, moving + +Moving files to a different directory or renaming them +is not difficult, but some of the ways in which this +works may be non-obvious. (Moving or renaming a +directory is even harder. @xref{Moving directories}.). + +The examples below assume that the file @var{old} is renamed to +@var{new}. + +@menu +* Outside:: The normal way to Rename +* Inside:: A tricky, alternative way +* Rename by copying:: Another tricky, alternative way +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Outside +@subsection The Normal way to Rename + +@c More rename issues. Not sure whether these are +@c worth documenting; I'm putting them here because +@c it seems to be as good a place as any to try to +@c set down the issues. +@c * "cvs annotate" will annotate either the new +@c file or the old file; it cannot annotate _each +@c line_ based on whether it was last changed in the +@c new or old file. Unlike "cvs log", where the +@c consequences of having to select either the new +@c or old name seem fairly benign, this may be a +@c real advantage to having CVS know about renames +@c other than as a deletion and an addition. + +The normal way to move a file is to copy @var{old} to +@var{new}, and then issue the normal @sc{cvs} commands +to remove @var{old} from the repository, and add +@var{new} to it. +@c The following sentence is not true: one must cd into +@c the directory to run "cvs add". +@c (Both @var{old} and @var{new} could +@c contain relative paths, for example @file{foo/bar.c}). + +@example +$ mv @var{old} @var{new} +$ cvs remove @var{old} +$ cvs add @var{new} +$ cvs commit -m "Renamed @var{old} to @var{new}" @var{old} @var{new} +@end example + +This is the simplest way to move a file, it is not +error-prone, and it preserves the history of what was +done. Note that to access the history of the file you +must specify the old or the new name, depending on what +portion of the history you are accessing. For example, +@code{cvs log @var{old}} will give the log up until the +time of the rename. + +When @var{new} is committed its revision numbers will +start again, usually at 1.1, so if that bothers you, +use the @samp{-r rev} option to commit. For more +information see @ref{Assigning revisions}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Inside +@subsection Moving the history file + +This method is more dangerous, since it involves moving +files inside the repository. Read this entire section +before trying it out! + +@example +$ cd $CVSROOT/@var{dir} +$ mv @var{old},v @var{new},v +@end example + +@noindent +Advantages: + +@itemize @bullet +@item +The log of changes is maintained intact. + +@item +The revision numbers are not affected. +@end itemize + +@noindent +Disadvantages: + +@itemize @bullet +@item +Old releases cannot easily be fetched from the +repository. (The file will show up as @var{new} even +in revisions from the time before it was renamed). + +@item +There is no log information of when the file was renamed. + +@item +Nasty things might happen if someone accesses the history file +while you are moving it. Make sure no one else runs any of the @sc{cvs} +commands while you move it. +@end itemize + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Rename by copying +@subsection Copying the history file + +This way also involves direct modifications to the +repository. It is safe, but not without drawbacks. + +@example +# @r{Copy the @sc{rcs} file inside the repository} +$ cd $CVSROOT/@var{dir} +$ cp @var{old},v @var{new},v +# @r{Remove the old file} +$ cd ~/@var{dir} +$ rm @var{old} +$ cvs remove @var{old} +$ cvs commit @var{old} +# @r{Remove all tags from @var{new}} +$ cvs update @var{new} +$ cvs log @var{new} # @r{Remember the non-branch tag names} +$ cvs tag -d @var{tag1} @var{new} +$ cvs tag -d @var{tag2} @var{new} +@dots{} +@end example + +By removing the tags you will be able to check out old +revisions. + +@noindent +Advantages: + +@itemize @bullet +@item +@c FIXME: Is this true about -D now that we have death +@c support? See 5B.3 in the FAQ. +Checking out old revisions works correctly, as long as +you use @samp{-r@var{tag}} and not @samp{-D@var{date}} +to retrieve the revisions. + +@item +The log of changes is maintained intact. + +@item +The revision numbers are not affected. +@end itemize + +@noindent +Disadvantages: + +@itemize @bullet +@item +You cannot easily see the history of the file across the rename. + +@ignore +@c Is this true? I don't see how the revision numbers +@c _could_ start over, when new,v is just old,v with +@c the tags deleted. +@c If there is some need to reinstate this text, +@c it is "usually 1.1", not "1.0" and it needs an +@c xref to Assigning revisions +@item +Unless you use the @samp{-r rev} (@pxref{commit +options}) flag when @var{new} is committed its revision +numbers will start at 1.0 again. +@end ignore +@end itemize + +@c --------------------------------------------------------------------- +@node Moving directories +@section Moving and renaming directories +@cindex Moving directories +@cindex Renaming directories +@cindex Directories, moving + +The normal way to rename or move a directory is to +rename or move each file within it as described in +@ref{Outside}. Then check out with the @samp{-P} +option, as described in @ref{Removing directories}. + +If you really want to hack the repository to rename or +delete a directory in the repository, you can do it +like this: + +@enumerate +@item +Inform everyone who has a checked out copy of the directory that the +directory will be renamed. They should commit all +their changes, and remove their working copies, +before you take the steps below. + +@item +Rename the directory inside the repository. + +@example +$ cd $CVSROOT/@var{parent-dir} +$ mv @var{old-dir} @var{new-dir} +@end example + +@item +Fix the @sc{cvs} administrative files, if necessary (for +instance if you renamed an entire module). + +@item +Tell everyone that they can check out again and continue +working. + +@end enumerate + +If someone had a working copy the @sc{cvs} commands will +cease to work for him, until he removes the directory +that disappeared inside the repository. + +It is almost always better to move the files in the +directory instead of moving the directory. If you move the +directory you are unlikely to be able to retrieve old +releases correctly, since they probably depend on the +name of the directories. + +@c --------------------------------------------------------------------- +@node History browsing +@chapter History browsing +@cindex History browsing +@cindex Traceability +@cindex Isolation + +@ignore +@c This is too long for an introduction (goal is +@c one 20x80 character screen), and also mixes up a +@c variety of issues (parallel development, history, +@c maybe even touches on process control). + +@c -- @quote{To lose ones history is to lose ones soul.} +@c -- /// +@c -- ///Those who cannot remember the past are condemned to repeat it. +@c -- /// -- George Santayana +@c -- /// + +@sc{cvs} tries to make it easy for a group of people to work +together. This is done in two ways: + +@itemize @bullet +@item +Isolation---You have your own working copy of the +source. You are not affected by modifications made by +others until you decide to incorporate those changes +(via the @code{update} command---@pxref{update}). + +@item +Traceability---When something has changed, you can +always see @emph{exactly} what changed. +@end itemize + +There are several features of @sc{cvs} that together lead +to traceability: + +@itemize @bullet +@item +Each revision of a file has an accompanying log +message. + +@item +All commits are optionally logged to a central history +database. + +@item +Logging information can be sent to a user-defined +program (@pxref{loginfo}). +@end itemize + +@c -- More text here. + +This chapter should talk about the history file, the +@code{log} command, the usefulness of ChangeLogs +even when you run @sc{cvs}, and things like that. + +@end ignore + +@c kind of lame, in a lot of ways the above text inside +@c the @ignore motivates this chapter better +Once you have used @sc{cvs} to store a version control +history---what files have changed when, how, and by +whom, there are a variety of mechanisms for looking +through the history. + +@c FIXME: should also be talking about how you look at +@c old revisions (e.g. "cvs update -p -r 1.2 foo.c"). +@menu +* log messages:: Log messages +* history database:: The history database +* user-defined logging:: User-defined logging +* annotate:: What revision modified each line of a file? +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node log messages +@section Log messages + +@c FIXME: @xref to place where we talk about how to +@c specify message to commit. +Whenever you commit a file you specify a log message. + +@c FIXME: bring the information here, and get rid of or +@c greatly shrink the "log" node. +To look through the log messages which have been +specified for every revision which has been committed, +use the @code{cvs log} command (@pxref{log}). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node history database +@section The history database + +@c FIXME: bring the information from the history file +@c and history nodes here. Rewrite it to be motivated +@c better (start out by clearly explaining what gets +@c logged in history, for example). +You can use the history file (@pxref{history file}) to +log various @sc{cvs} actions. To retrieve the +information from the history file, use the @code{cvs +history} command (@pxref{history}). +@c +@c The history database has many problems: +@c * It is very unclear what field means what. This +@c could be improved greatly by better documentation, +@c but there are still non-orthogonalities (for +@c example, tag does not record the "repository" +@c field but most records do). +@c * Confusion about files, directories, and modules. +@c Some commands record one, some record others. +@c * File removal is not logged. There is an 'R' +@c record type documented, but CVS never uses it. +@c * Tags are only logged for the "cvs rtag" command, +@c not "cvs tag". The fix for this is not completely +@c clear (see above about modules vs. files). +@c * Are there other cases of operations that are not +@c logged? One would hope for all changes to the +@c repository to be logged somehow (particularly +@c operations like tagging, "cvs admin -k", and other +@c operations which do not record a history that one +@c can get with "cvs log"). Operations on the working +@c directory, like export, get, and release, are a +@c second category also covered by the current "cvs +@c history". +@c * The history file does not record the options given +@c to a command. The most serious manifestation of +@c this is perhaps that it doesn't record whether a command +@c was recursive. It is not clear to me whether one +@c wants to log at a level very close to the command +@c line, as a sort of way of logging each command +@c (more or less), or whether one wants +@c to log more at the level of what was changed (or +@c something in between), but either way the current +@c information has pretty big gaps. +@c * Further details about a tag--like whether it is a +@c branch tag or, if a non-branch tag, which branch it +@c is on. One can find out this information about the +@c tag as it exists _now_, but if the tag has been +@c moved, one doesn't know what it was like at the time +@c the history record was written. +@c * Whether operating on a particular tag, date, or +@c options was implicit (sticky) or explicit. +@c +@c Another item, only somewhat related to the above, is a +@c way to control what is logged in the history file. +@c This is probably the only good way to handle +@c different people having different ideas about +@c information/space tradeoffs. +@c +@c It isn't really clear that it makes sense to try to +@c patch up the history file format as it exists now to +@c include all that stuff. It might be better to +@c design a whole new CVSROOT/nhistory file and "cvs +@c nhistory" command, or some such, or in some other +@c way trying to come up with a clean break from the +@c past, which can address the above concerns. Another +@c open question is how/whether this relates to +@c taginfo/loginfo/etc. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node user-defined logging +@section User-defined logging + +@c FIXME: should probably also mention the fact the -l +@c global option can disable most of the mechanisms +@c discussed here (why? What is the -l global option for?). +@c +@c FIXME: probably should centralize this information +@c here, at least to some extent. Maybe by moving the +@c loginfo, etc., nodes here and replacing +@c the "user-defined logging" node with one node for +@c each method. +You can customize @sc{cvs} to log various kinds of +actions, in whatever manner you choose. These +mechanisms operate by executing a script at various +times. The script might append a message to a file +listing the information and the programmer who created +it, or send mail to a group of developers, or, perhaps, +post a message to a particular newsgroup. To log +commits, use the @file{loginfo} file (@pxref{loginfo}). +@c FIXME: What is difference between doing it in the +@c modules file and using loginfo/taginfo? Why should +@c user use one or the other? +To log commits, checkouts, exports, and tags, +respectively, you can also use the @samp{-i}, +@samp{-o}, @samp{-e}, and @samp{-t} options in the +modules file. For a more flexible way of giving +notifications to various users, which requires less in +the way of keeping centralized scripts up to date, use +the @code{cvs watch add} command (@pxref{Getting +Notified}); this command is useful even if you are not +using @code{cvs watch on}. + +@cindex taginfo +@cindex Exit status, of taginfo +The @file{taginfo} file defines programs to execute +when someone executes a @code{tag} or @code{rtag} +command. The @file{taginfo} file has the standard form +for administrative files (@pxref{Administrative +files}), where each line is a regular expression +followed by a command to execute. The arguments passed +to the command are, in order, the @var{tagname}, +@var{operation} (@code{add} for @code{tag}, +@code{mov} for @code{tag -F}, and @code{del} for +@code{tag -d}), @var{repository}, and any remaining are +pairs of @var{filename} @var{revision}. A non-zero +exit of the filter program will cause the tag to be +aborted. + +Here is an example of using taginfo to log tag and rtag +commands. In the taginfo file put: + +@example +ALL /usr/local/cvsroot/CVSROOT/loggit +@end example + +Where @file{/usr/local/cvsroot/CVSROOT/loggit} contains the +following script: + +@example +#!/bin/sh +echo "$@@" >>/home/kingdon/cvsroot/CVSROOT/taglog +@end example + +@node annotate +@section Annotate command +@cindex annotate (subcommand) + +@deffn Command {cvs annotate} [@code{-flR}] [@code{-r rev}|@code{-D date}] files @dots{} + +For each file in @var{files}, print the head revision +of the trunk, together with information on the last +modification for each line. For example: + +@example +$ cvs annotate ssfile +Annotations for ssfile +*************** +1.1 (mary 27-Mar-96): ssfile line 1 +1.2 (joe 28-Mar-96): ssfile line 2 +@end example + +The file @file{ssfile} currently contains two lines. +The @code{ssfile line 1} line was checked in by +@code{mary} on March 27. Then, on March 28, @code{joe} +added a line @code{ssfile line 2}, without modifying +the @code{ssfile line 1} line. This report doesn't +tell you anything about lines which have been deleted +or replaced; you need to use @code{cvs diff} for that +(@pxref{diff}). + +@end deffn + +The options to @code{cvs annotate} are listed in +@ref{Invoking CVS}, and can be used to select the files +and revisions to annotate. The options are described +in more detail in @ref{Common options}. + +@c FIXME: maybe an example using the options? Just +@c what it means to select a revision might be worth a +@c few words of explanation ("you want to see who +@c changed this line *before* 1.4"...). + +@c --------------------------------------------------------------------- +@node Binary files +@chapter Handling binary files +@cindex Binary files + +The most common use for @sc{cvs} is to store text +files. With text files, @sc{cvs} can merge revisions, +display the differences between revisions in a +human-visible fashion, and other such operations. +However, if you are willing to give up a few of these +abilities, @sc{cvs} can store binary files. For +example, one might store a web site in @sc{cvs} +including both text files and binary images. + +@menu +* Binary why:: More details on issues with binary files +* Binary howto:: How to store them +@end menu + +@node Binary why +@section The issues with binary files + +While the need to manage binary files may seem obvious +if the files that you customarily work with are binary, +putting them into version control does present some +additional issues. + +One basic function of version control is to show the +differences between two revisions. For example, if +someone else checked in a new version of a file, you +may wish to look at what they changed and determine +whether their changes are good. For text files, +@sc{cvs} provides this functionality via the @code{cvs +diff} command. For binary files, it may be possible to +extract the two revisions and then compare them with a +tool external to @sc{cvs} (for example, word processing +software often has such a feature). If there is no +such tool, one must track changes via other mechanisms, +such as urging people to write good log messages, and +hoping that the changes they actually made were the +changes that they intended to make. + +Another ability of a version control system is the +ability to merge two revisions. For @sc{cvs} this +happens in two contexts. The first is when users make +changes in separate working directories +(@pxref{Multiple developers}). The second is when one +merges explicitly with the @samp{update -j} command +(@pxref{Branching and merging}). + +In the case of text +files, @sc{cvs} can merge changes made independently, +and signal a conflict if the changes conflict. With +binary files, the best that @sc{cvs} can do is present +the two different copies of the file, and leave it to +the user to resolve the conflict. The user may choose +one copy or the other, or may run an external merge +tool which knows about that particular file format, if +one exists. +Note that having the user merge relies primarily on the +user to not accidentally omit some changes, and thus is +potentially error prone. + +If this process is thought to be undesirable, the best +choice may be to avoid merging. To avoid the merges +that result from separate working directories, see the +discussion of reserved checkouts (file locking) in +@ref{Multiple developers}. To avoid the merges +resulting from branches, restrict use of branches. + +@node Binary howto +@section How to store binary files + +There are two issues with using @sc{cvs} to store +binary files. The first is that @sc{cvs} by default +converts line endings between the canonical form in +which they are stored in the repository (linefeed +only), and the form appropriate to the operating system +in use on the client (for example, carriage return +followed by line feed for Windows NT). + +The second is that a binary file might happen to +contain data which looks like a keyword (@pxref{Keyword +substitution}), so keyword expansion must be turned +off. + +@c FIXME: the third is that one can't do merges with +@c binary files. xref to Multiple Developers and the +@c reserved checkout issues. + +The @samp{-kb} option available with some @sc{cvs} +commands insures that neither line ending conversion +nor keyword expansion will be done. + +Here is an example of how you can create a new file +using the @samp{-kb} flag: + +@example +$ echo '$@asis{}Id$' > kotest +$ cvs add -kb -m"A test file" kotest +$ cvs ci -m"First checkin; contains a keyword" kotest +@end example + +If a file accidentally gets added without @samp{-kb}, +one can use the @code{cvs admin} command to recover. +For example: + +@example +$ echo '$@asis{}Id$' > kotest +$ cvs add -m"A test file" kotest +$ cvs ci -m"First checkin; contains a keyword" kotest +$ cvs admin -kb kotest +$ cvs update -A kotest +# @r{For non-unix systems:} +# @r{Copy in a good copy of the file from outside CVS} +$ cvs commit -m "make it binary" kotest +@end example + +@c Trying to describe this for both unix and non-unix +@c in the same description is very confusing. Might +@c want to split the two, or just ditch the unix "shortcut" +@c (unixheads don't do much with binary files, anyway). +@c This used to say "(Try the above example, and do a +@c @code{cat kotest} after every command)". But that +@c only really makes sense for the unix case. +When you check in the file @file{kotest} the file is +not preserved as a binary file, because you did not +check it in as a binary file. The @code{cvs +admin -kb} command sets the default keyword +substitution method for this file, but it does not +alter the working copy of the file that you have. If you need to +cope with line endings (that is, you are using +@sc{cvs} on a non-unix system), then you need to +check in a new copy of the file, as shown by the +@code{cvs commit} command above. +On unix, the @code{cvs update -A} command suffices. +@c FIXME: should also describe what the *other users* +@c need to do, if they have checked out copies which +@c have been corrupted by lack of -kb. I think maybe +@c "cvs update -kb" or "cvs +@c update -A" would suffice, although the user who +@c reported this suggested removing the file, manually +@c removing it from CVS/Entries, and then "cvs update" + +However, in using @code{cvs admin -k} to change the +keyword expansion, be aware that the keyword expansion +mode is not version controlled. This means that, for +example, that if you have a text file in old releases, +and a binary file with the same name in new releases, +@sc{cvs} provides no way to check out the file in text +or binary mode depending on what version you are +checking out. There is no good workaround for this +problem. + +You can also set a default for whether @code{cvs add} +and @code{cvs import} treat a file as binary based on +its name; for example you could say that files who +names end in @samp{.exe} are binary. @xref{Wrappers}. +There is currently no way to have @sc{cvs} detect +whether a file is binary based on its contents. The +main difficulty with designing such a feature is that +it is not clear how to distinguish between binary and +non-binary files, and the rules to apply would vary +considerably with the operating system. +@c For example, it would be good on MS-DOS-family OSes +@c for anything containing ^Z to be binary. Having +@c characters with the 8th bit set imply binary is almost +@c surely a bad idea in the context of ISO-8859-* and +@c other such character sets. On VMS or the Mac, we +@c could use the OS's file typing. This is a +@c commonly-desired feature, and something of this sort +@c may make sense. But there are a lot of pitfalls here. +@c +@c Another, probably better, way to tell is to read the +@c file in text mode, write it to a temp file in text +@c mode, and then do a binary mode compare of the two +@c files. If they differ, it is a binary file. This +@c might have problems on VMS (or some other system +@c with several different text modes), but in general +@c should be relatively portable. The only other +@c downside I can think of is that it would be fairly +@c slow, but that is perhaps a small price to pay for +@c not having your files corrupted. Another issue is +@c what happens if you import a text file with bare +@c linefeeds on Windows. Such files will show up on +@c Windows sometimes (I think some native windows +@c programs even write them, on occasion). Perhaps it +@c is reasonable to treat such files as binary; after +@c all it is something of a presumption to assume that +@c the user would want the linefeeds converted to CRLF. + +@c --------------------------------------------------------------------- +@node Multiple developers +@chapter Multiple developers +@cindex Multiple developers +@cindex Team of developers +@cindex File locking +@cindex Locking files +@cindex Working copy +@cindex Reserved checkouts +@cindex Unreserved checkouts +@cindex RCS-style locking + +When more than one person works on a software project +things often get complicated. Often, two people try to +edit the same file simultaneously. One solution, known +as @dfn{file locking} or @dfn{reserved checkouts}, is +to allow only one person to edit each file at a time. +This is the only solution with some version control +systems, including @sc{rcs} and @sc{sccs}. Currently +the usual way to get reserved checkouts with @sc{cvs} +is the @code{cvs admin -l} command (@pxref{admin +options}). This is not as nicely integrated into +@sc{cvs} as the watch features, described below, but it +seems that most people with a need for reserved +checkouts find it adequate. +@c Or "find it better than worrying about implementing +@c nicely integrated reserved checkouts" or ...? +It also may be possible to use the watches +features described below, together with suitable +procedures (not enforced by software), to avoid having +two people edit at the same time. + +@c Our unreserved checkout model might not +@c be quite the same as others. For example, I +@c think that some systems will tend to create a branch +@c in the case where CVS prints "up-to-date check failed". +@c It isn't clear to me whether we should try to +@c explore these subtleties; it could easily just +@c confuse people. +The default model with @sc{cvs} is known as +@dfn{unreserved checkouts}. In this model, developers +can edit their own @dfn{working copy} of a file +simultaneously. The first person that commits his +changes has no automatic way of knowing that another +has started to edit it. Others will get an error +message when they try to commit the file. They must +then use @sc{cvs} commands to bring their working copy +up to date with the repository revision. This process +is almost automatic. + +@c FIXME? should probably use the word "watch" here, to +@c tie this into the text below and above. +@sc{cvs} also supports mechanisms which facilitate +various kinds of communication, without actually +enforcing rules like reserved checkouts do. + +The rest of this chapter describes how these various +models work, and some of the issues involved in +choosing between them. + +@ignore +Here is a draft reserved checkout design or discussion +of the issues. This seems like as good a place as any +for this. + +Might want a cvs lock/cvs unlock--in which the names +differ from edit/unedit because the network must be up +for these to work. unedit gives an error if there is a +reserved checkout in place (so that people don't +accidentally leave locks around); unlock gives an error +if one is not in place (this is more arguable; perhaps +it should act like unedit in that case). + +On the other hand, might want it so that emacs, +scripts, etc., can get ready to edit a file without +having to know which model is in use. In that case we +would have a "cvs watch lock" (or .cvsrc?) (that is, +three settings, "on", "off", and "lock"). Having cvs +watch lock set would cause a get to record in the CVS +directory which model is in use, and cause "cvs edit" +to change behaviors. We'd want a way to query which +setting is in effect (this would be handy even if it is +only "on" or "off" as presently). If lock is in +effect, then commit would require a lock before +allowing a checkin; chmod wouldn't suffice (might be +debatable--see chmod comment below, in watches--but it +is the way people expect RCS to work and I can't think +of any significant downside. On the other hand, maybe +it isn't worth bothering, because people who are used +to RCS wouldn't think to use chmod anyway). + +Implementation: use file attributes or use RCS +locking. The former avoids more dependence on RCS +behaviors we will need to reimplement as we librarify +RCS, and makes it easier to import/export RCS files (in +that context, want to ignore the locker field). But +note that RCS locks are per-branch, which is the +correct behavior (this is also an issue for the "watch +on" features; they should be per-branch too). + +Here are a few more random notes about implementation +details, assuming "cvs watch lock" and + +CVS/Watched file? Or try to fit this into CVS/Entries somehow? +Cases: (1) file is checked out (unreserved or with watch on) by old +version of CVS, now we do something with new one, (2) file is checked +out by new version, now we do something with old one. + +Remote protocol would have a "Watched" analogous to "Mode". Of course +it would apply to all Updated-like requests. How do we keep this +setting up to date? I guess that there wants to be a Watched request, +and the server would send a new one if it isn't up to date? (Ugh--hard +to implement and slows down "cvs -q update"--is there an easier way?) + +"cvs edit"--checks CVS/Watched, and if watch lock, then sends +"edit-lock" request. Which comes back with a Checked-in with +appropriate Watched (off, on, lock, locked, or some such?), or error +message if already locked. + +"cvs commit"--only will commit if off/on/locked. lock is not OK. + +Doc: +note that "cvs edit" must be connected to network if watch lock is in +effect. + +Talk about what to do if someone has locked a file and you want to +edit that file. (breaking locks, or lack thereof). + + +One other idea (which could work along with the +existing "cvs admin -l" reserved checkouts, as well as +the above): + +"cvs editors" could show who has the file locked, if +someone does. + +@end ignore + +@menu +* File status:: A file can be in several states +* Updating a file:: Bringing a file up-to-date +* Conflicts example:: An informative example +* Informing others:: To cooperate you must inform +* Concurrency:: Simultaneous repository access +* Watches:: Mechanisms to track who is editing files +* Choosing a model:: Reserved or unreserved checkouts? +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node File status +@section File status +@cindex File status +@cindex Status of a file + +@c Shouldn't this start with an example or something, +@c introducing the unreserved checkout model? Before we +@c dive into listing states? +Based on what operations you have performed on a +checked out file, and what operations others have +performed to that file in the repository, one can +classify a file in a number of states. The states, as +reported by the @code{status} command, are: + +@c The order of items is chosen to group logically +@c similar outputs together. +@c People who want alphabetical can use the index... +@table @asis +@cindex Up-to-date +@item Up-to-date +The file is identical with the latest revision in the +repository for the branch in use. +@c FIXME: should we clarify "in use"? The answer is +@c sticky tags, and trying to distinguish branch sticky +@c tags from non-branch sticky tags seems rather awkward +@c here. +@c FIXME: What happens with non-branch sticky tags? Is +@c a stuck file "Up-to-date" or "Needs checkout" or what? + +@item Locally Modified +@cindex Locally Modified +You have edited the file, and not yet committed your changes. + +@item Locally Added +@cindex Locally Added +You have added the file with @code{add}, and not yet +committed your changes. +@c There are many cases involving the file being +@c added/removed/modified in the working directory, and +@c added/removed/modified in the repository, which we +@c don't try to describe here. I'm not sure that "cvs +@c status" produces a non-confusing output in most of +@c those cases. + +@item Locally Removed +@cindex Locally Removed +You have removed the file with @code{remove}, and not yet +committed your changes. + +@item Needs Checkout +@cindex Needs Checkout +Someone else has committed a newer revision to the +repository. The name is slightly misleading; you will +ordinarily use @code{update} rather than +@code{checkout} to get that newer revision. + +@item Needs Patch +@cindex Needs Patch +@c See also newb-123j0 in sanity.sh (although that case +@c should probably be changed rather than documented). +Like Needs Checkout, but the @sc{cvs} server will send +a patch rather than the entire file. Sending a patch or +sending an entire file accomplishes the same thing. + +@item Needs Merge +@cindex Needs Merge +Someone else has committed a newer revision to the repository, and you +have also made modifications to the file. + +@item File had conflicts on merge +@cindex File had conflicts on merge +@c is it worth saying that this message was "Unresolved +@c Conflict" in CVS 1.9 and earlier? I'm inclined to +@c think that is unnecessarily confusing to new users. +This is like Locally Modified, except that a previous +@code{update} command gave a conflict. If you have not +already done so, you need to +resolve the conflict as described in @ref{Conflicts example}. + +@item Unknown +@cindex Unknown +@sc{cvs} doesn't know anything about this file. For +example, you have created a new file and have not run +@code{add}. +@c +@c "Entry Invalid" and "Classify Error" are also in the +@c status.c. The latter definitely indicates a CVS bug +@c (should it be worded more like "internal error" so +@c people submit bug reports if they see it?). The former +@c I'm not as sure; I haven't tracked down whether/when it +@c appears in "cvs status" output. + +@end table + +To help clarify the file status, @code{status} also +reports the @code{Working revision} which is the +revision that the file in the working directory derives +from, and the @code{Repository revision} which is the +latest revision in the repository for the branch in +use. +@c FIXME: should we clarify "in use"? The answer is +@c sticky tags, and trying to distinguish branch sticky +@c tags from non-branch sticky tags seems rather awkward +@c here. +@c FIXME: What happens with non-branch sticky tags? +@c What is the Repository Revision there? See the +@c comment at vn_rcs in cvs.h, which is kind of +@c confused--we really need to document better what this +@c field contains. +@c Q: Should we document "New file!" and other such +@c outputs or are they self-explanatory? +@c FIXME: what about the date to the right of "Working +@c revision"? It doesn't appear with client/server and +@c seems unnecessary (redundant with "ls -l") so +@c perhaps it should be removed for non-client/server too? +@c FIXME: Need some examples. +@c FIXME: Working revision can also be something like +@c "-1.3" for a locally removed file. Not at all +@c self-explanatory (and it is possible that CVS should +@c be changed rather than documenting this). + +@c Would be nice to have an @example showing output +@c from cvs status, with comments showing the xref +@c where each part of the output is described. This +@c might fit in nicely if it is desirable to split this +@c node in two; one to introduce "cvs status" and one +@c to list each of the states. +The options to @code{status} are listed in +@ref{Invoking CVS}. For information on its @code{Sticky tag} +and @code{Sticky date} output, see @ref{Sticky tags}. +For information on its @code{Sticky options} output, +see the @samp{-k} option in @ref{update options}. + +You can think of the @code{status} and @code{update} +commands as somewhat complementary. You use +@code{update} to bring your files up to date, and you +can use @code{status} to give you some idea of what an +@code{update} would do (of course, the state of the +repository might change before you actually run +@code{update}). In fact, if you want a command to +display file status in a more brief format than is +displayed by the @code{status} command, you can invoke + +@cindex update, to display file status +@example +$ cvs -n -q update +@end example + +The @samp{-n} option means to not actually do the +update, but merely to display statuses; the @samp{-q} +option avoids printing the name of each directory. For +more information on the @code{update} command, and +these options, see @ref{Invoking CVS}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Updating a file +@section Bringing a file up to date +@cindex Bringing a file up to date +@cindex Updating a file +@cindex Merging a file +@cindex Update, introduction + +When you want to update or merge a file, use the @code{update} +command. For files that are not up to date this is roughly equivalent +to a @code{checkout} command: the newest revision of the file is +extracted from the repository and put in your working directory. + +Your modifications to a file are never lost when you +use @code{update}. If no newer revision exists, +running @code{update} has no effect. If you have +edited the file, and a newer revision is available, +@sc{cvs} will merge all changes into your working copy. + +For instance, imagine that you checked out revision 1.4 and started +editing it. In the meantime someone else committed revision 1.5, and +shortly after that revision 1.6. If you run @code{update} on the file +now, @sc{cvs} will incorporate all changes between revision 1.4 and 1.6 into +your file. + +@cindex Overlap +If any of the changes between 1.4 and 1.6 were made too +close to any of the changes you have made, an +@dfn{overlap} occurs. In such cases a warning is +printed, and the resulting file includes both +versions of the lines that overlap, delimited by +special markers. +@xref{update}, for a complete description of the +@code{update} command. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Conflicts example +@section Conflicts example +@cindex Merge, an example +@cindex Example of merge +@cindex driver.c (merge example) + +Suppose revision 1.4 of @file{driver.c} contains this: + +@example +#include <stdio.h> + +void main() +@{ + parse(); + if (nerr == 0) + gencode(); + else + fprintf(stderr, "No code generated.\n"); + exit(nerr == 0 ? 0 : 1); +@} +@end example + +@noindent +Revision 1.6 of @file{driver.c} contains this: + +@example +#include <stdio.h> + +int main(int argc, + char **argv) +@{ + parse(); + if (argc != 1) + @{ + fprintf(stderr, "tc: No args expected.\n"); + exit(1); + @} + if (nerr == 0) + gencode(); + else + fprintf(stderr, "No code generated.\n"); + exit(!!nerr); +@} +@end example + +@noindent +Your working copy of @file{driver.c}, based on revision +1.4, contains this before you run @samp{cvs update}: +@c -- Really include "cvs"? + +@example +#include <stdlib.h> +#include <stdio.h> + +void main() +@{ + init_scanner(); + parse(); + if (nerr == 0) + gencode(); + else + fprintf(stderr, "No code generated.\n"); + exit(nerr == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +@} +@end example + +@noindent +You run @samp{cvs update}: +@c -- Really include "cvs"? + +@example +$ cvs update driver.c +RCS file: /usr/local/cvsroot/yoyodyne/tc/driver.c,v +retrieving revision 1.4 +retrieving revision 1.6 +Merging differences between 1.4 and 1.6 into driver.c +rcsmerge warning: overlaps during merge +cvs update: conflicts found in driver.c +C driver.c +@end example + +@noindent +@cindex Conflicts (merge example) +@sc{cvs} tells you that there were some conflicts. +Your original working file is saved unmodified in +@file{.#driver.c.1.4}. The new version of +@file{driver.c} contains this: + +@example +#include <stdlib.h> +#include <stdio.h> + +int main(int argc, + char **argv) +@{ + init_scanner(); + parse(); + if (argc != 1) + @{ + fprintf(stderr, "tc: No args expected.\n"); + exit(1); + @} + if (nerr == 0) + gencode(); + else + fprintf(stderr, "No code generated.\n"); +@asis{}<<<<<<< driver.c + exit(nerr == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +@asis{}======= + exit(!!nerr); +@asis{}>>>>>>> 1.6 +@} +@end example + +@noindent +@cindex Markers, conflict +@cindex Conflict markers +@cindex <<<<<<< +@cindex >>>>>>> +@cindex ======= + +Note how all non-overlapping modifications are incorporated in your working +copy, and that the overlapping section is clearly marked with +@samp{<<<<<<<}, @samp{=======} and @samp{>>>>>>>}. + +@cindex Resolving a conflict +@cindex Conflict resolution +You resolve the conflict by editing the file, removing the markers and +the erroneous line. Suppose you end up with this file: +@c -- Add xref to the pcl-cvs manual when it talks +@c -- about this. +@example +#include <stdlib.h> +#include <stdio.h> + +int main(int argc, + char **argv) +@{ + init_scanner(); + parse(); + if (argc != 1) + @{ + fprintf(stderr, "tc: No args expected.\n"); + exit(1); + @} + if (nerr == 0) + gencode(); + else + fprintf(stderr, "No code generated.\n"); + exit(nerr == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +@} +@end example + +@noindent +You can now go ahead and commit this as revision 1.7. + +@example +$ cvs commit -m "Initialize scanner. Use symbolic exit values." driver.c +Checking in driver.c; +/usr/local/cvsroot/yoyodyne/tc/driver.c,v <-- driver.c +new revision: 1.7; previous revision: 1.6 +done +@end example + +For your protection, @sc{cvs} will refuse to check in a +file if a conflict occurred and you have not resolved +the conflict. Currently to resolve a conflict, you +must change the timestamp on the file. In previous +versions of @sc{cvs}, you also needed to +insure that the file contains no conflict markers. +Because +your file may legitimately contain conflict markers (that +is, occurrences of @samp{>>>>>>> } at the start of a +line that don't mark a conflict), the current +version of @sc{cvs} will print a warning and proceed to +check in the file. +@c The old behavior was really icky; the only way out +@c was to start hacking on +@c the @code{CVS/Entries} file or other such workarounds. +@c +@c If the timestamp thing isn't considered nice enough, +@c maybe there should be a "cvs resolved" command +@c which clears the conflict indication. For a nice user +@c interface, this should be invoked by an interactive +@c merge tool like emerge rather than by the user +@c directly--such a tool can verify that the user has +@c really dealt with each conflict. + +@cindex emerge +If you use release 1.04 or later of pcl-cvs (a @sc{gnu} +Emacs front-end for @sc{cvs}) you can use an Emacs +package called emerge to help you resolve conflicts. +See the documentation for pcl-cvs. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Informing others +@section Informing others about commits +@cindex Informing others +@cindex Spreading information +@cindex Mail, automatic mail on commit + +It is often useful to inform others when you commit a +new revision of a file. The @samp{-i} option of the +@file{modules} file, or the @file{loginfo} file, can be +used to automate this process. @xref{modules}. +@xref{loginfo}. You can use these features of @sc{cvs} +to, for instance, instruct @sc{cvs} to mail a +message to all developers, or post a message to a local +newsgroup. +@c -- More text would be nice here. + +@node Concurrency +@section Several developers simultaneously attempting to run CVS + +@cindex Locks, cvs, introduction +@c For a discussion of *why* CVS creates locks, see +@c the comment at the start of src/lock.c +If several developers try to run @sc{cvs} at the same +time, one may get the following message: + +@example +[11:43:23] waiting for bach's lock in /usr/local/cvsroot/foo +@end example + +@cindex #cvs.rfl, removing +@cindex #cvs.wfl, removing +@cindex #cvs.lock, removing +@sc{cvs} will try again every 30 seconds, and either +continue with the operation or print the message again, +if it still needs to wait. If a lock seems to stick +around for an undue amount of time, find the person +holding the lock and ask them about the cvs command +they are running. If they aren't running a cvs +command, look in the repository directory mentioned in +the message and remove files which they own whose names +start with @file{#cvs.rfl}, +@file{#cvs.wfl}, or @file{#cvs.lock}. + +Note that these locks are to protect @sc{cvs}'s +internal data structures and have no relationship to +the word @dfn{lock} in the sense used by +@sc{rcs}---which refers to reserved checkouts +(@pxref{Multiple developers}). + +Any number of people can be reading from a given +repository at a time; only when someone is writing do +the locks prevent other people from reading or writing. + +@cindex Atomic transactions, lack of +@cindex Transactions, atomic, lack of +@c the following talks about what one might call commit/update +@c atomicity. +@c Probably also should say something about +@c commit/commit atomicity, that is, "An update will +@c not get partial versions of more than one commit". +@c CVS currently has this property and I guess we can +@c make it a documented feature. +@c For example one person commits +@c a/one.c and b/four.c and another commits a/two.c and +@c b/three.c. Then an update cannot get the new a/one.c +@c and a/two.c and the old b/four.c and b/three.c. +One might hope for the following property + +@example +If someone commits some changes in one cvs command, +then an update by someone else will either get all the +changes, or none of them. +@end example + +but @sc{cvs} does @emph{not} have this property. For +example, given the files + +@example +a/one.c +a/two.c +b/three.c +b/four.c +@end example + +if someone runs + +@example +cvs ci a/two.c b/three.c +@end example + +and someone else runs @code{cvs update} at the same +time, the person running @code{update} might get only +the change to @file{b/three.c} and not the change to +@file{a/two.c}. + +@node Watches +@section Mechanisms to track who is editing files +@cindex Watches + +For many groups, use of @sc{cvs} in its default mode is +perfectly satisfactory. Users may sometimes go to +check in a modification only to find that another +modification has intervened, but they deal with it and +proceed with their check in. Other groups prefer to be +able to know who is editing what files, so that if two +people try to edit the same file they can choose to +talk about who is doing what when rather than be +surprised at check in time. The features in this +section allow such coordination, while retaining the +ability of two developers to edit the same file at the +same time. + +@c Some people might ask why CVS does not enforce the +@c rule on chmod, by requiring a cvs edit before a cvs +@c commit. The main reason is that it could always be +@c circumvented--one could edit the file, and +@c then when ready to check it in, do the cvs edit and put +@c in the new contents and do the cvs commit. One +@c implementation note: if we _do_ want to have cvs commit +@c require a cvs edit, we should store the state on +@c whether the cvs edit has occurred in the working +@c directory, rather than having the server try to keep +@c track of what working directories exist. +@c FIXME: should the above discussion be part of the +@c manual proper, somewhere, not just in a comment? +For maximum benefit developers should use @code{cvs +edit} (not @code{chmod}) to make files read-write to +edit them, and @code{cvs release} (not @code{rm}) to +discard a working directory which is no longer in use, +but @sc{cvs} is not able to enforce this behavior. + +@c I'm a little dissatisfied with this presentation, +@c because "watch on"/"edit"/"editors" are one set of +@c functionality, and "watch add"/"watchers" is another +@c which is somewhat orthogonal even though they interact in +@c various ways. But I think it might be +@c confusing to describe them separately (e.g. "watch +@c add" with loginfo). I don't know. + +@menu +* Setting a watch:: Telling CVS to watch certain files +* Getting Notified:: Telling CVS to notify you +* Editing files:: How to edit a file which is being watched +* Watch information:: Information about who is watching and editing +* Watches Compatibility:: Watches interact poorly with CVS 1.6 or earlier +@end menu + +@node Setting a watch +@subsection Telling CVS to watch certain files + +To enable the watch features, you first specify that +certain files are to be watched. + +@cindex watch on (subcommand) +@deffn Command {cvs watch on} [@code{-lR}] files @dots{} + +@cindex Read-only files, and watches +Specify that developers should run @code{cvs edit} +before editing @var{files}. CVS will create working +copies of @var{files} read-only, to remind developers +to run the @code{cvs edit} command before working on +them. + +If @var{files} includes the name of a directory, CVS +arranges to watch all files added to the corresponding +repository directory, and sets a default for files +added in the future; this allows the user to set +notification policies on a per-directory basis. The +contents of the directory are processed recursively, +unless the @code{-l} option is given. +The @code{-R} option can be used to force recursion if the @code{-l} +option is set in @file{~/.cvsrc} (@pxref{~/.cvsrc}). + +If @var{files} is omitted, it defaults to the current directory. + +@cindex watch off (subcommand) +@end deffn + +@deffn Command {cvs watch off} [@code{-lR}] files @dots{} + +Do not create @var{files} read-only on checkout; thus, +developers will not be reminded to use @code{cvs edit} +and @code{cvs unedit}. CVS will check out @var{files} +read-write as usual, unless other permissions override +due to the @code{PreservePermissions} option being +enabled in the @file{config} administrative file +(@pxref{Special Files}, @pxref{config}) + +The @var{files} and options are processed as for @code{cvs +watch on}. + +@end deffn + +@node Getting Notified +@subsection Telling CVS to notify you + +You can tell @sc{cvs} that you want to receive +notifications about various actions taken on a file. +You can do this without using @code{cvs watch on} for +the file, but generally you will want to use @code{cvs +watch on}, so that developers use the @code{cvs edit} +command. + +@cindex watch add (subcommand) +@deffn Command {cvs watch add} [@code{-a} action] [@code{-lR}] files @dots{} + +Add the current user to the list of people to receive notification of +work done on @var{files}. + +The @code{-a} option specifies what kinds of events CVS should notify +the user about. @var{action} is one of the following: + +@table @code + +@item edit +Another user has applied the @code{cvs edit} command (described +below) to a file. + +@item unedit +Another user has applied the @code{cvs unedit} command (described +below) or the @code{cvs release} command to a file, or has deleted +the file and allowed @code{cvs update} to recreate it. + +@item commit +Another user has committed changes to a file. + +@item all +All of the above. + +@item none +None of the above. (This is useful with @code{cvs edit}, +described below.) + +@end table + +The @code{-a} option may appear more than once, or not at all. If +omitted, the action defaults to @code{all}. + +The @var{files} and options are processed as for the +@code{cvs watch} commands. + +@end deffn + + +@cindex watch remove (subcommand) +@deffn Command {cvs watch remove} [@code{-a} action] [@code{-lR}] files @dots{} + +Remove a notification request established using @code{cvs watch add}; +the arguments are the same. If the @code{-a} option is present, only +watches for the specified actions are removed. + +@end deffn + +@cindex notify (admin file) +When the conditions exist for notification, @sc{cvs} +calls the @file{notify} administrative file. Edit +@file{notify} as one edits the other administrative +files (@pxref{Intro administrative files}). This +file follows the usual conventions for administrative +files (@pxref{syntax}), where each line is a regular +expression followed by a command to execute. The +command should contain a single occurrence of @samp{%s} +which will be replaced by the user to notify; the rest +of the information regarding the notification will be +supplied to the command on standard input. The +standard thing to put in the @code{notify} file is the +single line: + +@example +ALL mail %s -s \"CVS notification\" +@end example + +This causes users to be notified by electronic mail. +@c FIXME: should it be this hard to set up this +@c behavior (and the result when one fails to do so, +@c silent failure to notify, so non-obvious)? Should +@c CVS give a warning if no line in notify matches (and +@c document the use of "DEFAULT :" for the case where +@c skipping the notification is indeed desired)? + +@cindex users (admin file) +Note that if you set this up in the straightforward +way, users receive notifications on the server machine. +One could of course write a @file{notify} script which +directed notifications elsewhere, but to make this +easy, @sc{cvs} allows you to associate a notification +address for each user. To do so create a file +@file{users} in @file{CVSROOT} with a line for each +user in the format @var{user}:@var{value}. Then +instead of passing the name of the user to be notified +to @file{notify}, @sc{cvs} will pass the @var{value} +(normally an email address on some other machine). + +@sc{cvs} does not notify you for your own changes. +Currently this check is done based on whether the user +name of the person taking the action which triggers +notification matches the user name of the person +getting notification. In fact, in general, the watches +features only track one edit by each user. It probably +would be more useful if watches tracked each working +directory separately, so this behavior might be worth +changing. +@c "behavior might be worth changing" is an effort to +@c point to future directions while also not promising +@c that "they" (as in "why don't they fix CVS to....") +@c will do this. +@c one implementation issue is identifying whether a +@c working directory is same or different. Comparing +@c pathnames/hostnames is hopeless, but having the server +@c supply a serial number which the client stores in the +@c CVS directory as a magic cookie should work. + +@node Editing files +@subsection How to edit a file which is being watched + +@cindex Checkout, as term for getting ready to edit +Since a file which is being watched is checked out +read-only, you cannot simply edit it. To make it +read-write, and inform others that you are planning to +edit it, use the @code{cvs edit} command. Some systems +call this a @dfn{checkout}, but @sc{cvs} uses that term +for obtaining a copy of the sources (@pxref{Getting the +source}), an operation which those systems call a +@dfn{get} or a @dfn{fetch}. +@c Issue to think about: should we transition CVS +@c towards the "get" terminology? "cvs get" is already a +@c synonym for "cvs checkout" and that section of the +@c manual refers to "Getting the source". If this is +@c done, needs to be done gingerly (for example, we should +@c still accept "checkout" in .cvsrc files indefinitely +@c even if the CVS's messages are changed from "cvs checkout: " +@c to "cvs get: "). +@c There is a concern about whether "get" is not as +@c good for novices because it is a more general term +@c than "checkout" (and thus arguably harder to assign +@c a technical meaning for). + +@cindex edit (subcommand) +@deffn Command {cvs edit} [options] files @dots{} + +Prepare to edit the working files @var{files}. CVS makes the +@var{files} read-write, and notifies users who have requested +@code{edit} notification for any of @var{files}. + +The @code{cvs edit} command accepts the same @var{options} as the +@code{cvs watch add} command, and establishes a temporary watch for the +user on @var{files}; CVS will remove the watch when @var{files} are +@code{unedit}ed or @code{commit}ted. If the user does not wish to +receive notifications, she should specify @code{-a none}. + +The @var{files} and options are processed as for the @code{cvs +watch} commands. + +@strong{Caution:} If the @code{PreservePermissions} +option is enabled in the repository (@pxref{config}), +CVS will not change the permissions on any of the +@var{files}. The reason for this change is to ensure +that using @samp{cvs edit} does not interfere with the +ability to store file permissions in the CVS +repository. + +@end deffn + +Normally when you are done with a set of changes, you +use the @code{cvs commit} command, which checks in your +changes and returns the watched files to their usual +read-only state. But if you instead decide to abandon +your changes, or not to make any changes, you can use +the @code{cvs unedit} command. + +@cindex unedit (subcommand) +@cindex Abandoning work +@cindex Reverting to repository version +@deffn Command {cvs unedit} [@code{-lR}] files @dots{} + +Abandon work on the working files @var{files}, and revert them to the +repository versions on which they are based. CVS makes those +@var{files} read-only for which users have requested notification using +@code{cvs watch on}. CVS notifies users who have requested @code{unedit} +notification for any of @var{files}. + +The @var{files} and options are processed as for the +@code{cvs watch} commands. + +If watches are not in use, the @code{unedit} command +probably does not work, and the way to revert to the +repository version is to remove the file and then use +@code{cvs update} to get a new copy. The meaning is +not precisely the same; removing and updating may also +bring in some changes which have been made in the +repository since the last time you updated. +@c It would be a useful enhancement to CVS to make +@c unedit work in the non-watch case as well. +@end deffn + +When using client/server @sc{cvs}, you can use the +@code{cvs edit} and @code{cvs unedit} commands even if +@sc{cvs} is unable to successfully communicate with the +server; the notifications will be sent upon the next +successful @sc{cvs} command. + +@node Watch information +@subsection Information about who is watching and editing + +@cindex watchers (subcommand) +@deffn Command {cvs watchers} [@code{-lR}] files @dots{} + +List the users currently watching changes to @var{files}. The report +includes the files being watched, and the mail address of each watcher. + +The @var{files} and options are processed as for the +@code{cvs watch} commands. + +@end deffn + + +@cindex editors (subcommand) +@deffn Command {cvs editors} [@code{-lR}] files @dots{} + +List the users currently working on @var{files}. The report +includes the mail address of each user, the time when the user began +working with the file, and the host and path of the working directory +containing the file. + +The @var{files} and options are processed as for the +@code{cvs watch} commands. + +@end deffn + +@node Watches Compatibility +@subsection Using watches with old versions of CVS + +@cindex CVS 1.6, and watches +If you use the watch features on a repository, it +creates @file{CVS} directories in the repository and +stores the information about watches in that directory. +If you attempt to use @sc{cvs} 1.6 or earlier with the +repository, you get an error message such as the +following (all on one line): + +@example +cvs update: cannot open CVS/Entries for reading: +No such file or directory +@end example + +and your operation will likely be aborted. To use the +watch features, you must upgrade all copies of @sc{cvs} +which use that repository in local or server mode. If +you cannot upgrade, use the @code{watch off} and +@code{watch remove} commands to remove all watches, and +that will restore the repository to a state which +@sc{cvs} 1.6 can cope with. + +@node Choosing a model +@section Choosing between reserved or unreserved checkouts +@cindex Choosing, reserved or unreserved checkouts + +Reserved and unreserved checkouts each have pros and +cons. Let it be said that a lot of this is a matter of +opinion or what works given different groups' working +styles, but here is a brief description of some of the +issues. There are many ways to organize a team of +developers. @sc{cvs} does not try to enforce a certain +organization. It is a tool that can be used in several +ways. + +Reserved checkouts can be very counter-productive. If +two persons want to edit different parts of a file, +there may be no reason to prevent either of them from +doing so. Also, it is common for someone to take out a +lock on a file, because they are planning to edit it, +but then forget to release the lock. + +@c "many groups"? specifics? cites to papers on this? +@c some way to weasel-word it a bit more so we don't +@c need facts :-)? +People, especially people who are familiar with +reserved checkouts, often wonder how often conflicts +occur if unreserved checkouts are used, and how +difficult they are to resolve. The experience with +many groups is that they occur rarely and usually are +relatively straightforward to resolve. + +The rarity of serious conflicts may be surprising, until one realizes +that they occur only when two developers disagree on the proper design +for a given section of code; such a disagreement suggests that the +team has not been communicating properly in the first place. In order +to collaborate under @emph{any} source management regimen, developers +must agree on the general design of the system; given this agreement, +overlapping changes are usually straightforward to merge. + +In some cases unreserved checkouts are clearly +inappropriate. If no merge tool exists for the kind of +file you are managing (for example word processor files +or files edited by Computer Aided Design programs), and +it is not desirable to change to a program which uses a +mergeable data format, then resolving conflicts is +going to be unpleasant enough that you generally will +be better off to simply avoid the conflicts instead, by +using reserved checkouts. + +The watches features described above in @ref{Watches} +can be considered to be an intermediate model between +reserved checkouts and unreserved checkouts. When you +go to edit a file, it is possible to find out who else +is editing it. And rather than having the system +simply forbid both people editing the file, it can tell +you what the situation is and let you figure out +whether it is a problem in that particular case or not. +Therefore, for some groups it can be considered the +best of both the reserved checkout and unreserved +checkout worlds. + +@c --------------------------------------------------------------------- +@node Revision management +@chapter Revision management +@cindex Revision management + +@c -- This chapter could be expanded a lot. +@c -- Experiences are very welcome! + +If you have read this far, you probably have a pretty +good grasp on what @sc{cvs} can do for you. This +chapter talks a little about things that you still have +to decide. + +If you are doing development on your own using @sc{cvs} +you could probably skip this chapter. The questions +this chapter takes up become more important when more +than one person is working in a repository. + +@menu +* When to commit:: Some discussion on the subject +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node When to commit +@section When to commit? +@cindex When to commit +@cindex Commit, when to +@cindex Policy + +Your group should decide which policy to use regarding +commits. Several policies are possible, and as your +experience with @sc{cvs} grows you will probably find +out what works for you. + +If you commit files too quickly you might commit files +that do not even compile. If your partner updates his +working sources to include your buggy file, he will be +unable to compile the code. On the other hand, other +persons will not be able to benefit from the +improvements you make to the code if you commit very +seldom, and conflicts will probably be more common. + +It is common to only commit files after making sure +that they can be compiled. Some sites require that the +files pass a test suite. Policies like this can be +enforced using the commitinfo file +(@pxref{commitinfo}), but you should think twice before +you enforce such a convention. By making the +development environment too controlled it might become +too regimented and thus counter-productive to the real +goal, which is to get software written. + +@c --------------------------------------------------------------------- +@node Keyword substitution +@chapter Keyword substitution +@cindex Keyword substitution +@cindex Keyword expansion +@cindex Identifying files + +@comment Be careful when editing this chapter. +@comment Remember that this file is kept under +@comment version control, so we must not accidentally +@comment include a valid keyword in the running text. + +As long as you edit source files inside a working +directory you can always find out the state of +your files via @samp{cvs status} and @samp{cvs log}. +But as soon as you export the files from your +development environment it becomes harder to identify +which revisions they are. + +CVS can use a mechanism known as @dfn{keyword +substitution} (or @dfn{keyword expansion}) to help +identifying the files. Embedded strings of the form +@code{$@var{keyword}$} and +@code{$@var{keyword}:@dots{}$} in a file are replaced +with strings of the form +@code{$@var{keyword}:@var{value}$} whenever you obtain +a new revision of the file. + +@menu +* Keyword list:: Keywords +* Using keywords:: Using keywords +* Avoiding substitution:: Avoiding substitution +* Substitution modes:: Substitution modes +* Log keyword:: Problems with the $@asis{}Log$ keyword. +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Keyword list +@section Keyword List +@cindex Keyword List + +@c FIXME: need some kind of example here I think, +@c perhaps in a +@c "Keyword intro" node. The intro in the "Keyword +@c substitution" node itself seems OK, but to launch +@c into a list of the keywords somehow seems too abrupt. + +This is a list of the keywords: + +@table @code +@cindex Author keyword +@item $@asis{Author}$ +The login name of the user who checked in the revision. + +@cindex Date keyword +@item $@asis{Date}$ +The date and time (UTC) the revision was checked in. + +@cindex Header keyword +@item $@asis{Header}$ +A standard header containing the full pathname of the +@sc{rcs} file, the revision number, the date (UTC), the +author, the state, and the locker (if locked). Files +will normally never be locked when you use @sc{cvs}. + +@cindex Id keyword +@item $@asis{Id}$ +Same as @code{$@asis{Header}$}, except that the @sc{rcs} +filename is without a path. + +@cindex Name keyword +@item $@asis{Name}$ +Tag name used to check out this file. The keyword is +expanded only if one checks out with an explicit tag +name. For example, when running the command @code{cvs +co -r first}, the keyword expands to @samp{Name: first}. + +@cindex Locker keyword +@item $@asis{Locker}$ +The login name of the user who locked the revision +(empty if not locked, which is the normal case unless +@code{cvs admin -l} is in use). + +@cindex Log keyword +@item $@asis{Log}$ +The log message supplied during commit, preceded by a +header containing the @sc{rcs} filename, the revision +number, the author, and the date (UTC). Existing log +messages are @emph{not} replaced. Instead, the new log +message is inserted after @code{$@asis{Log:@dots{}}$}. +Each new line is prefixed with the same string which +precedes the @code{$Log} keyword. For example, if the +file contains + +@example + /* Here is what people have been up to: + * + * $@asis{}Log: frob.c,v $ + * Revision 1.1 1997/01/03 14:23:51 joe + * Add the superfrobnicate option + * + */ +@end example + +@noindent +then additional lines which are added when expanding +the @code{$Log} keyword will be preceded by @samp{ * }. +Unlike previous versions of @sc{cvs} and @sc{rcs}, the +@dfn{comment leader} from the @sc{rcs} file is not used. +The @code{$Log} keyword is useful for +accumulating a complete change log in a source file, +but for several reasons it can be problematic. +@xref{Log keyword}. + +@cindex RCSfile keyword +@item $@asis{RCSfile}$ +The name of the RCS file without a path. + +@cindex Revision keyword +@item $@asis{Revision}$ +The revision number assigned to the revision. + +@cindex Source keyword +@item $@asis{Source}$ +The full pathname of the RCS file. + +@cindex State keyword +@item $@asis{State}$ +The state assigned to the revision. States can be +assigned with @code{cvs admin -s}---see @ref{admin options}. + +@end table + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Using keywords +@section Using keywords + +To include a keyword string you simply include the +relevant text string, such as @code{$@asis{Id}$}, inside the +file, and commit the file. @sc{cvs} will automatically +expand the string as part of the commit operation. + +It is common to embed the @code{$@asis{}Id$} string in +the source files so that it gets passed through to +generated files. For example, if you are managing +computer program source code, you might include a +variable which is initialized to contain that string. +Or some C compilers may provide a @code{#pragma ident} +directive. Or a document management system might +provide a way to pass a string through to generated +files. + +@c Would be nice to give an example, but doing this in +@c portable C is not possible and the problem with +@c picking any one language (VMS HELP files, Ada, +@c troff, whatever) is that people use CVS for all +@c kinds of files. + +@cindex Ident (shell command) +The @code{ident} command (which is part of the @sc{rcs} +package) can be used to extract keywords and their +values from a file. This can be handy for text files, +but it is even more useful for extracting keywords from +binary files. + +@example +$ ident samp.c +samp.c: + $@asis{}Id: samp.c,v 1.5 1993/10/19 14:57:32 ceder Exp $ +$ gcc samp.c +$ ident a.out +a.out: + $@asis{}Id: samp.c,v 1.5 1993/10/19 14:57:32 ceder Exp $ +@end example + +@cindex What (shell command) +S@sc{ccs} is another popular revision control system. +It has a command, @code{what}, which is very similar to +@code{ident} and used for the same purpose. Many sites +without @sc{rcs} have @sc{sccs}. Since @code{what} +looks for the character sequence @code{@@(#)} it is +easy to include keywords that are detected by either +command. Simply prefix the keyword with the +magic @sc{sccs} phrase, like this: + +@example +static char *id="@@(#) $@asis{}Id: ab.c,v 1.5 1993/10/19 14:57:32 ceder Exp $"; +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Avoiding substitution +@section Avoiding substitution + +Keyword substitution has its disadvantages. Sometimes +you might want the literal text string +@samp{$@asis{}Author$} to appear inside a file without +@sc{cvs} interpreting it as a keyword and expanding it +into something like @samp{$@asis{}Author: ceder $}. + +There is unfortunately no way to selectively turn off +keyword substitution. You can use @samp{-ko} +(@pxref{Substitution modes}) to turn off keyword +substitution entirely. + +In many cases you can avoid using keywords in +the source, even though they appear in the final +product. For example, the source for this manual +contains @samp{$@@asis@{@}Author$} whenever the text +@samp{$@asis{}Author$} should appear. In @code{nroff} +and @code{troff} you can embed the null-character +@code{\&} inside the keyword for a similar effect. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Substitution modes +@section Substitution modes +@cindex Keyword substitution, changing modes +@cindex -k (keyword substitution) +@cindex Kflag + +@c FIXME: This could be made more coherent, by expanding it +@c with more examples or something. +Each file has a stored default substitution mode, and +each working directory copy of a file also has a +substitution mode. The former is set by the @samp{-k} +option to @code{cvs add} and @code{cvs admin}; the +latter is set by the @samp{-k} or @samp{-A} options to @code{cvs +checkout} or @code{cvs update}. @code{cvs diff} also +has a @samp{-k} option. For some examples, +see @ref{Binary files}, and @ref{Merging and keywords}. +@c The fact that -A is overloaded to mean both reset +@c sticky options and reset sticky tags/dates is +@c somewhat questionable. Perhaps there should be +@c separate options to reset sticky options (e.g. -k +@c A") and tags/dates (someone suggested -r HEAD could +@c do this instead of setting a sticky tag of "HEAD" +@c as in the status quo but I haven't thought much +@c about that idea. Of course -r .reset or something +@c could be coined if this needs to be a new option). +@c On the other hand, having -A mean "get things back +@c into the state after a fresh checkout" has a certain +@c appeal, and maybe there is no sufficient reason for +@c creeping featurism in this area. + +The modes available are: + +@table @samp +@item -kkv +Generate keyword strings using the default form, e.g. +@code{$@asis{}Revision: 5.7 $} for the @code{Revision} +keyword. + +@item -kkvl +Like @samp{-kkv}, except that a locker's name is always +inserted if the given revision is currently locked. +The locker's name is only relevant if @code{cvs admin +-l} is in use. + +@item -kk +Generate only keyword names in keyword strings; omit +their values. For example, for the @code{Revision} +keyword, generate the string @code{$@asis{}Revision$} +instead of @code{$@asis{}Revision: 5.7 $}. This option +is useful to ignore differences due to keyword +substitution when comparing different revisions of a +file (@pxref{Merging and keywords}). + +@item -ko +Generate the old keyword string, present in the working +file just before it was checked in. For example, for +the @code{Revision} keyword, generate the string +@code{$@asis{}Revision: 1.1 $} instead of +@code{$@asis{}Revision: 5.7 $} if that is how the +string appeared when the file was checked in. + +@item -kb +Like @samp{-ko}, but also inhibit conversion of line +endings between the canonical form in which they are +stored in the repository (linefeed only), and the form +appropriate to the operating system in use on the +client. For systems, like unix, which use linefeed +only to terminate lines, this is the same as +@samp{-ko}. For more information on binary files, see +@ref{Binary files}. + +@item -kv +Generate only keyword values for keyword strings. For +example, for the @code{Revision} keyword, generate the string +@code{5.7} instead of @code{$@asis{}Revision: 5.7 $}. +This can help generate files in programming languages +where it is hard to strip keyword delimiters like +@code{$@asis{}Revision: $} from a string. However, +further keyword substitution cannot be performed once +the keyword names are removed, so this option should be +used with care. + +One often would like to use @samp{-kv} with @code{cvs +export}---@pxref{export}. But be aware that doesn't +handle an export containing binary files correctly. + +@end table + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Log keyword +@section Problems with the $@asis{}Log$ keyword. + +The @code{$@asis{}Log$} keyword is somewhat +controversial. As long as you are working on your +development system the information is easily accessible +even if you do not use the @code{$@asis{}Log$} +keyword---just do a @code{cvs log}. Once you export +the file the history information might be useless +anyhow. + +A more serious concern is that @sc{cvs} is not good at +handling @code{$@asis{}Log$} entries when a branch is +merged onto the main trunk. Conflicts often result +from the merging operation. +@c Might want to check whether the CVS implementation +@c of RCS_merge has this problem the same way rcsmerge +@c does. I would assume so.... + +People also tend to "fix" the log entries in the file +(correcting spelling mistakes and maybe even factual +errors). If that is done the information from +@code{cvs log} will not be consistent with the +information inside the file. This may or may not be a +problem in real life. + +It has been suggested that the @code{$@asis{}Log$} +keyword should be inserted @emph{last} in the file, and +not in the files header, if it is to be used at all. +That way the long list of change messages will not +interfere with everyday source file browsing. + +@c --------------------------------------------------------------------- +@node Tracking sources +@chapter Tracking third-party sources +@cindex Third-party sources +@cindex Tracking sources + +@c FIXME: Need discussion of added and removed files. +@c FIXME: This doesn't really adequately introduce the +@c concepts of "vendor" and "you". They don't *have* +@c to be separate organizations or separate people. +@c We want a description which is somewhat more based on +@c the technical issues of which sources go where, but +@c also with enough examples of how this relates to +@c relationships like customer-supplier, developer-QA, +@c maintainer-contributor, or whatever, to make it +@c seem concrete. +If you modify a program to better fit your site, you +probably want to include your modifications when the next +release of the program arrives. @sc{cvs} can help you with +this task. + +@cindex Vendor +@cindex Vendor branch +@cindex Branch, vendor- +In the terminology used in @sc{cvs}, the supplier of the +program is called a @dfn{vendor}. The unmodified +distribution from the vendor is checked in on its own +branch, the @dfn{vendor branch}. @sc{cvs} reserves branch +1.1.1 for this use. + +When you modify the source and commit it, your revision +will end up on the main trunk. When a new release is +made by the vendor, you commit it on the vendor branch +and copy the modifications onto the main trunk. + +Use the @code{import} command to create and update +the vendor branch. When you import a new file, +the vendor branch is made the `head' revision, so +anyone that checks out a copy of the file gets that +revision. When a local modification is committed it is +placed on the main trunk, and made the `head' +revision. + +@menu +* First import:: Importing for the first time +* Update imports:: Updating with the import command +* Reverting local changes:: Reverting to the latest vendor release +* Binary files in imports:: Binary files require special handling +* Keywords in imports:: Keyword substitution might be undesirable +* Multiple vendor branches:: What if you get sources from several places? +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node First import +@section Importing for the first time +@cindex Importing modules + +@c Should mention naming conventions for vendor tags, +@c release tags, and perhaps directory names. +Use the @code{import} command to check in the sources +for the first time. When you use the @code{import} +command to track third-party sources, the @dfn{vendor +tag} and @dfn{release tags} are useful. The +@dfn{vendor tag} is a symbolic name for the branch +(which is always 1.1.1, unless you use the @samp{-b +@var{branch}} flag---@xref{Multiple vendor branches}.). The +@dfn{release tags} are symbolic names for a particular +release, such as @samp{FSF_0_04}. + +@c I'm not completely sure this belongs here. But +@c we need to say it _somewhere_ reasonably obvious; it +@c is a common misconception among people first learning CVS +Note that @code{import} does @emph{not} change the +directory in which you invoke it. In particular, it +does not set up that directory as a @sc{cvs} working +directory; if you want to work with the sources import +them first and then check them out into a different +directory (@pxref{Getting the source}). + +@cindex wdiff (import example) +Suppose you have the sources to a program called +@code{wdiff} in a directory @file{wdiff-0.04}, +and are going to make private modifications that you +want to be able to use even when new releases are made +in the future. You start by importing the source to +your repository: + +@example +$ cd wdiff-0.04 +$ cvs import -m "Import of FSF v. 0.04" fsf/wdiff FSF_DIST WDIFF_0_04 +@end example + +The vendor tag is named @samp{FSF_DIST} in the above +example, and the only release tag assigned is +@samp{WDIFF_0_04}. +@c FIXME: Need to say where fsf/wdiff comes from. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Update imports +@section Updating with the import command + +When a new release of the source arrives, you import it into the +repository with the same @code{import} command that you used to set up +the repository in the first place. The only difference is that you +specify a different release tag this time. + +@example +$ tar xfz wdiff-0.05.tar.gz +$ cd wdiff-0.05 +$ cvs import -m "Import of FSF v. 0.05" fsf/wdiff FSF_DIST WDIFF_0_05 +@end example + +For files that have not been modified locally, the newly created +revision becomes the head revision. If you have made local +changes, @code{import} will warn you that you must merge the changes +into the main trunk, and tell you to use @samp{checkout -j} to do so. + +@c FIXME: why "wdiff" here and "fsf/wdiff" in the +@c "import"? I think the assumption is that one has +@c "wdiff fsf/wdiff" or some such in modules, but it +@c would be better to not use modules in this example. +@example +$ cvs checkout -jFSF_DIST:yesterday -jFSF_DIST wdiff +@end example + +@noindent +The above command will check out the latest revision of +@samp{wdiff}, merging the changes made on the vendor branch @samp{FSF_DIST} +since yesterday into the working copy. If any conflicts arise during +the merge they should be resolved in the normal way (@pxref{Conflicts +example}). Then, the modified files may be committed. + +Using a date, as suggested above, assumes that you do +not import more than one release of a product per +day. If you do, you can always use something like this +instead: + +@example +$ cvs checkout -jWDIFF_0_04 -jWDIFF_0_05 wdiff +@end example + +@noindent +In this case, the two above commands are equivalent. + +@node Reverting local changes +@section Reverting to the latest vendor release + +You can also revert local changes completely and return +to the latest vendor release by changing the `head' +revision back to the vendor branch on all files. For +example, if you have a checked-out copy of the sources +in @file{~/work.d/wdiff}, and you want to revert to the +vendor's version for all the files in that directory, +you would type: + +@example +$ cd ~/work.d/wdiff +$ cvs admin -bWDIFF . +@end example + +@noindent +You must specify the @samp{-bWDIFF} without any space +after the @samp{-b}. @xref{admin options}. + +@node Binary files in imports +@section How to handle binary files with cvs import + +Use the @samp{-k} wrapper option to tell import which +files are binary. @xref{Wrappers}. + +@node Keywords in imports +@section How to handle keyword substitution with cvs import + +The sources which you are importing may contain +keywords (@pxref{Keyword substitution}). For example, +the vendor may use @sc{cvs} or some other system +which uses similar keyword expansion syntax. If you +just import the files in the default fashion, then +the keyword expansions supplied by the vendor will +be replaced by keyword expansions supplied by your +own copy of @sc{cvs}. It may be more convenient to +maintain the expansions supplied by the vendor, so +that this information can supply information about +the sources that you imported from the vendor. + +To maintain the keyword expansions supplied by the +vendor, supply the @samp{-ko} option to @code{cvs +import} the first time you import the file. +This will turn off keyword expansion +for that file entirely, so if you want to be more +selective you'll have to think about what you want +and use the @samp{-k} option to @code{cvs update} or +@code{cvs admin} as appropriate. +@c Supplying -ko to import if the file already existed +@c has no effect. Not clear to me whether it should +@c or not. + +@node Multiple vendor branches +@section Multiple vendor branches + +All the examples so far assume that there is only one +vendor from which you are getting sources. In some +situations you might get sources from a variety of +places. For example, suppose that you are dealing with +a project where many different people and teams are +modifying the software. There are a variety of ways to +handle this, but in some cases you have a bunch of +source trees lying around and what you want to do more +than anything else is just to all put them in CVS so +that you at least have them in one place. + +For handling situations in which there may be more than +one vendor, you may specify the @samp{-b} option to +@code{cvs import}. It takes as an argument the vendor +branch to import to. The default is @samp{-b 1.1.1}. + +For example, suppose that there are two teams, the red +team and the blue team, that are sending you sources. +You want to import the red team's efforts to branch +1.1.1 and use the vendor tag RED. You want to import +the blue team's efforts to branch 1.1.3 and use the +vendor tag BLUE. So the commands you might use are: + +@example +$ cvs import dir RED RED_1-0 +$ cvs import -b 1.1.3 dir BLUE BLUE_1-5 +@end example + +Note that if your vendor tag does not match your +@samp{-b} option, CVS will not detect this case! For +example, + +@example +$ cvs import -b 1.1.3 dir RED RED_1-0 +@end example + +@noindent +Be careful; this kind of mismatch is sure to sow +confusion or worse. I can't think of a useful purpose +for the ability to specify a mismatch here, but if you +discover such a use, don't. CVS is likely to make this +an error in some future release. + +@c Probably should say more about the semantics of +@c multiple branches. What about the default branch? +@c What about joining (perhaps not as useful with +@c multiple branches, or perhaps it is. Either way +@c should be mentioned). + +@c I'm not sure about the best location for this. In +@c one sense, it might belong right after we've introduced +@c CVS's basic version control model, because people need +@c to figure out builds right away. The current location +@c is based on the theory that it kind of akin to the +@c "Revision management" section. +@node Builds +@chapter How your build system interacts with CVS +@cindex Builds +@cindex make + +As mentioned in the introduction, @sc{cvs} does not +contain software for building your software from source +code. This section describes how various aspects of +your build system might interact with @sc{cvs}. + +@c Is there a way to discuss this without reference to +@c tools other than CVS? I'm not sure there is; I +@c wouldn't think that people who learn CVS first would +@c even have this concern. +One common question, especially from people who are +accustomed to @sc{rcs}, is how to make their build get +an up to date copy of the sources. The answer to this +with @sc{cvs} is two-fold. First of all, since +@sc{cvs} itself can recurse through directories, there +is no need to modify your @file{Makefile} (or whatever +configuration file your build tool uses) to make sure +each file is up to date. Instead, just use two +commands, first @code{cvs -q update} and then +@code{make} or whatever the command is to invoke your +build tool. Secondly, you do not necessarily +@emph{want} to get a copy of a change someone else made +until you have finished your own work. One suggested +approach is to first update your sources, then +implement, build and +test the change you were thinking of, and then commit +your sources (updating first if necessary). By +periodically (in between changes, using the approach +just described) updating your entire tree, you ensure +that your sources are sufficiently up to date. + +@cindex Bill of materials +One common need is to record which versions of which +source files went into a particular build. This kind +of functionality is sometimes called @dfn{bill of +materials} or something similar. The best way to do +this with @sc{cvs} is to use the @code{tag} command to +record which versions went into a given build +(@pxref{Tags}). + +Using @sc{cvs} in the most straightforward manner +possible, each developer will have a copy of the entire +source tree which is used in a particular build. If +the source tree is small, or if developers are +geographically dispersed, this is the preferred +solution. In fact one approach for larger projects is +to break a project down into smaller +@c I say subsystem instead of module because they may or +@c may not use the modules file. +separately-compiled subsystems, and arrange a way of +releasing them internally so that each developer need +check out only those subsystems which are they are +actively working on. + +Another approach is to set up a structure which allows +developers to have their own copies of some files, and +for other files to access source files from a central +location. Many people have come up with some such a +@c two such people are paul@sander.cupertino.ca.us (for +@c a previous employer) +@c and gtornblo@senet.abb.se (spicm and related tools), +@c but as far as I know +@c no one has nicely packaged or released such a system (or +@c instructions for constructing one). +system using features such as the symbolic link feature +found in many operating systems, or the @code{VPATH} +feature found in many versions of @code{make}. One build +tool which is designed to help with this kind of thing +is Odin (see +@code{ftp://ftp.cs.colorado.edu/pub/distribs/odin}). +@c Should we be saying more about Odin? Or how you use +@c it with CVS? Also, the Prime Time Freeware for Unix +@c disk (see http://www.ptf.com/) has Odin (with a nice +@c paragraph summarizing it on the web), so that might be a +@c semi-"official" place to point people. +@c +@c Of course, many non-CVS systems have this kind of +@c functionality, for example OSF's ODE +@c (http://www.osf.org/ode/) or mk +@c (http://www.grin.net/~pzi/mk-3.18.4.docs/mk_toc.html +@c He has changed providers in the past; a search engine search +@c for "Peter Ziobrzynski" probably won't get too many +@c spurious hits :-). A more stable URL might be +@c ftp://ftp.uu.net/pub/cmvc/mk). But I'm not sure +@c there is any point in mentioning them here unless they +@c can work with CVS. + +@c --------------------------------------------------------------------- +@node Special Files +@chapter Special Files + +@cindex Special files +@cindex Device nodes +@cindex Ownership, saving in CVS +@cindex Permissions, saving in CVS +@cindex Hard links +@cindex Symbolic links + +In normal circumstances, CVS works only with regular +files. Every file in a project is assumed to be +persistent; it must be possible to open, read and close +them; and so on. CVS also ignores file permissions and +ownerships, leaving such issues to be resolved by the +developer at installation time. In other words, it is +not possible to "check in" a device into a repository; +if the device file cannot be opened, CVS will refuse to +handle it. Files also lose their ownerships and +permissions during repository transactions. + +If the configuration variable @code{PreservePermissions} +(@pxref{config}) is set in the repository, CVS will +save the following file characteristics in the +repository: + +@itemize @bullet +@item user and group ownership +@item permissions +@item major and minor device numbers +@item symbolic links +@item hard link structure +@end itemize + +Using the @code{PreservePermissions} option affects the +behavior of CVS in several ways. First, some of the +new operations supported by CVS are not accessible to +all users. In particular, file ownership and special +file characteristics may only be changed by the +superuser. When the @code{PreservePermissions} +configuration variable is set, therefore, users will +have to be `root' in order to perform CVS operations. + +When @code{PreservePermissions} is in use, some CVS +operations (such as @samp{cvs status}) will not +recognize a file's hard link structure, and so will +emit spurious warnings about mismatching hard links. +The reason is that CVS's internal structure does not +make it easy for these operations to collect all the +necessary data about hard links, so they check for file +conflicts with inaccurate data. + +A more subtle difference is that CVS considers a file +to have changed only if its contents have changed +(specifically, if the modification time of the working +file does not match that of the repository's file). +Therefore, if only the permissions, ownership or hard +linkage have changed, or if a device's major or minor +numbers have changed, CVS will not notice. In order to +commit such a change to the repository, you must force +the commit with @samp{cvs commit -f}. This also means +that if a file's permissions have changed and the +repository file is newer than the working copy, +performing @samp{cvs update} will silently change the +permissions on the working copy. + +Changing hard links in a CVS repository is particularly +delicate. Suppose that file @file{foo} is linked to +file @file{old}, but is later relinked to file +@file{new}. You can wind up in the unusual situation +where, although @file{foo}, @file{old} and @file{new} +have all had their underlying link patterns changed, +only @file{foo} and @file{new} have been modified, so +@file{old} is not considered a candidate for checking +in. It can be very easy to produce inconsistent +results this way. Therefore, we recommend that when it +is important to save hard links in a repository, the +prudent course of action is to @code{touch} any file +whose linkage or status has changed since the last +checkin. Indeed, it may be wise to @code{touch *} +before each commit in a directory with complex hard +link structures. + +It is worth noting that only regular files may +be merged, for reasons that hopefully are obvious. If +@samp{cvs update} or @samp{cvs checkout -j} attempts to +merge a symbolic link with a regular file, or two +device files for different kinds of devices, CVS will +report a conflict and refuse to perform the merge. At +the same time, @samp{cvs diff} will not report any +differences between these files, since no meaningful +textual comparisons can be made on files which contain +no text. + +The @code{PreservePermissions} features do not work +with client/server @sc{cvs}. Another limitation is +that hard links must be to other files within the same +directory; hard links across directories are not +supported. + +@c --------------------------------------------------------------------- +@node CVS commands +@appendix Guide to CVS commands + +This appendix describes the overall structure of +@sc{cvs} commands, and describes some commands in +detail (others are described elsewhere; for a quick +reference to @sc{cvs} commands, @pxref{Invoking CVS}). +@c The idea is that we want to move the commands which +@c are described here into the main body of the manual, +@c in the process reorganizing the manual to be +@c organized around what the user wants to do, not +@c organized around CVS commands. +@c +@c Note that many users do expect a manual which is +@c organized by command. At least some users do. +@c One good addition to the "organized by command" +@c section (if any) would be "see also" links. +@c The awk manual might be a good example; it has a +@c reference manual which is more verbose than Invoking +@c CVS but probably somewhat less verbose than CVS +@c Commands. + +@menu +* Structure:: Overall structure of CVS commands +* Exit status:: Indicating CVS's success or failure +* ~/.cvsrc:: Default options with the ~/.csvrc file +* Global options:: Options you give to the left of cvs_command +* Common options:: Options you give to the right of cvs_command +* admin:: Administration +* checkout:: Checkout sources for editing +* commit:: Check files into the repository +* diff:: Show differences between revisions +* export:: Export sources from CVS, similar to checkout +* history:: Show status of files and users +* import:: Import sources into CVS, using vendor branches +* log:: Show log messages for files +* rdiff:: 'patch' format diffs between releases +* release:: Indicate that a directory is no longer in use +* update:: Bring work tree in sync with repository +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Structure +@appendixsec Overall structure of CVS commands +@cindex Structure +@cindex CVS command structure +@cindex Command structure +@cindex Format of CVS commands + +The overall format of all @sc{cvs} commands is: + +@example +cvs [ cvs_options ] cvs_command [ command_options ] [ command_args ] +@end example + +@table @code +@item cvs +The name of the @sc{cvs} program. + +@item cvs_options +Some options that affect all sub-commands of @sc{cvs}. These are +described below. + +@item cvs_command +One of several different sub-commands. Some of the commands have +aliases that can be used instead; those aliases are noted in the +reference manual for that command. There are only two situations +where you may omit @samp{cvs_command}: @samp{cvs -H} elicits a +list of available commands, and @samp{cvs -v} displays version +information on @sc{cvs} itself. + +@item command_options +Options that are specific for the command. + +@item command_args +Arguments to the commands. +@end table + +There is unfortunately some confusion between +@code{cvs_options} and @code{command_options}. +@samp{-l}, when given as a @code{cvs_option}, only +affects some of the commands. When it is given as a +@code{command_option} is has a different meaning, and +is accepted by more commands. In other words, do not +take the above categorization too seriously. Look at +the documentation instead. + +@node Exit status +@appendixsec CVS's exit status +@cindex Exit status, of CVS + +CVS can indicate to the calling environment whether it +succeeded or failed by setting its @dfn{exit status}. +The exact way of testing the exit status will vary from +one operating system to another. For example in a unix +shell script the @samp{$?} variable will be 0 if the +last command returned a successful exit status, or +greater than 0 if the exit status indicated failure. + +If CVS is successful, it returns a successful status; +if there is an error, it prints an error message and +returns a failure status. The one exception to this is +the @code{cvs diff} command. It will return a +successful status if it found no differences, or a +failure status if there were differences or if there +was an error. Because this behavior provides no good +way to detect errors, in the future it is possible that +@code{cvs diff} will be changed to behave like the +other @sc{cvs} commands. +@c It might seem like checking whether cvs -q diff +@c produces empty or non-empty output can tell whether +@c there were differences or not. But it seems like +@c there are cases with output but no differences +@c (testsuite basica-8b). It is not clear to me how +@c useful it is for a script to be able to check +@c whether there were differences. +@c FIXCVS? In previous versions of CVS, cvs diff +@c returned 0 for no differences, 1 for differences, or +@c 2 for errors. Is this behavior worth trying to +@c bring back (but what does it mean for VMS?)? + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node ~/.cvsrc +@appendixsec Default options and the ~/.cvsrc file +@cindex .cvsrc file +@cindex Option defaults + +There are some @code{command_options} that are used so +often that you might have set up an alias or some other +means to make sure you always specify that option. One +example (the one that drove the implementation of the +@file{.cvsrc} support, actually) is that many people find the +default output of the @samp{diff} command to be very +hard to read, and that either context diffs or unidiffs +are much easier to understand. + +The @file{~/.cvsrc} file is a way that you can add +default options to @code{cvs_commands} within cvs, +instead of relying on aliases or other shell scripts. + +The format of the @file{~/.cvsrc} file is simple. The +file is searched for a line that begins with the same +name as the @code{cvs_command} being executed. If a +match is found, then the remainder of the line is split +up (at whitespace characters) into separate options and +added to the command arguments @emph{before} any +options from the command line. + +If a command has two names (e.g., @code{checkout} and +@code{co}), the official name, not necessarily the one +used on the command line, will be used to match against +the file. So if this is the contents of the user's +@file{~/.cvsrc} file: + +@example +log -N +diff -u +update -P +checkout -P +@end example + +@noindent +the command @samp{cvs checkout foo} would have the +@samp{-P} option added to the arguments, as well as +@samp{cvs co foo}. + +With the example file above, the output from @samp{cvs +diff foobar} will be in unidiff format. @samp{cvs diff +-c foobar} will provide context diffs, as usual. +Getting "old" format diffs would be slightly more +complicated, because @code{diff} doesn't have an option +to specify use of the "old" format, so you would need +@samp{cvs -f diff foobar}. + +In place of the command name you can use @code{cvs} to +specify global options (@pxref{Global options}). For +example the following line in @file{.cvsrc} + +@example +cvs -z6 +@end example + +causes @sc{cvs} to use compression level 6. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Global options +@appendixsec Global options +@cindex Options, global +@cindex Global options +@cindex Left-hand options + +The available @samp{cvs_options} (that are given to the +left of @samp{cvs_command}) are: + +@table @code +@item --allow-root=@var{rootdir} +Specify legal @sc{cvsroot} directory. See +@ref{Password authentication server}. + +@cindex Authentication, stream +@cindex Stream authentication +@item -a +Authenticate all communication between the client and +the server. Only has an effect on the @sc{cvs} client. +As of this writing, this is only implemented when using +a GSSAPI connection (@pxref{GSSAPI authenticated}). +Authentication prevents certain sorts of attacks +involving hijacking the active @sc{tcp} connection. +Enabling authentication does not enable encryption. + +@cindex RCSBIN, overriding +@cindex Overriding RCSBIN +@item -b @var{bindir} +In @sc{cvs} 1.9.18 and older, this specified that +@sc{rcs} programs are in the @var{bindir} directory. +Current versions of @sc{cvs} do not run @sc{rcs} +programs; for compatibility this option is accepted, +but it does nothing. + +@cindex TMPDIR, overriding +@cindex Overriding TMPDIR +@item -T @var{tempdir} +Use @var{tempdir} as the directory where temporary files are +located. Overrides the setting of the @code{$TMPDIR} environment +variable and any precompiled directory. This parameter should be +specified as an absolute pathname. + +@cindex CVSROOT, overriding +@cindex Overriding CVSROOT +@item -d @var{cvs_root_directory} +Use @var{cvs_root_directory} as the root directory +pathname of the repository. Overrides the setting of +the @code{$CVSROOT} environment variable. @xref{Repository}. + +@cindex EDITOR, overriding +@cindex Overriding EDITOR +@item -e @var{editor} +Use @var{editor} to enter revision log information. Overrides the +setting of the @code{$CVSEDITOR} and @code{$EDITOR} +environment variables. For more information, see +@ref{Committing your changes}. + +@item -f +Do not read the @file{~/.cvsrc} file. This +option is most often used because of the +non-orthogonality of the @sc{cvs} option set. For +example, the @samp{cvs log} option @samp{-N} (turn off +display of tag names) does not have a corresponding +option to turn the display on. So if you have +@samp{-N} in the @file{~/.cvsrc} entry for @samp{log}, +you may need to use @samp{-f} to show the tag names. + +@item -H +@itemx --help +Display usage information about the specified @samp{cvs_command} +(but do not actually execute the command). If you don't specify +a command name, @samp{cvs -H} displays overall help for +@sc{cvs}, including a list of other help options. +@c It seems to me it is better to document it this way +@c rather than trying to update this documentation +@c every time that we add a --help-foo option. But +@c perhaps that is confusing... + +@item -l +Do not log the @samp{cvs_command} in the command history (but execute it +anyway). @xref{history}, for information on command history. + +@cindex Read-only mode +@item -n +Do not change any files. Attempt to execute the +@samp{cvs_command}, but only to issue reports; do not remove, +update, or merge any existing files, or create any new files. + +Note that @sc{cvs} will not necessarily produce exactly +the same output as without @samp{-n}. In some cases +the output will be the same, but in other cases +@sc{cvs} will skip some of the processing that would +have been required to produce the exact same output. + +@item -Q +Cause the command to be really quiet; the command will only +generate output for serious problems. + +@item -q +Cause the command to be somewhat quiet; informational messages, +such as reports of recursion through subdirectories, are +suppressed. + +@cindex Read-only files, and -r +@item -r +Make new working files read-only. Same effect +as if the @code{$CVSREAD} environment variable is set +(@pxref{Environment variables}). The default is to +make working files writable, unless watches are on +(@pxref{Watches}). + +@item -s @var{variable}=@var{value} +Set a user variable (@pxref{Variables}). + +@cindex Trace +@item -t +Trace program execution; display messages showing the steps of +@sc{cvs} activity. Particularly useful with @samp{-n} to explore the +potential impact of an unfamiliar command. + +@item -v +@item --version +Display version and copyright information for @sc{cvs}. + +@cindex CVSREAD, overriding +@cindex Overriding CVSREAD +@item -w +Make new working files read-write. Overrides the +setting of the @code{$CVSREAD} environment variable. +Files are created read-write by default, unless @code{$CVSREAD} is +set or @samp{-r} is given. +@c Note that -w only overrides -r and CVSREAD; it has +@c no effect on files which are readonly because of +@c "cvs watch on". My guess is that is the way it +@c should be (or should "cvs -w get" on a watched file +@c be the same as a get and a cvs edit?), but I'm not +@c completely sure whether to document it this way. + +@item -x +@cindex Encryption +Encrypt all communication between the client and the +server. Only has an effect on the @sc{cvs} client. As +of this writing, this is only implemented when using a +GSSAPI connection (@pxref{GSSAPI authenticated}) or a +Kerberos connection (@pxref{Kerberos authenticated}). +Enabling encryption implies that message traffic is +also authenticated. Encryption support is not +available by default; it must be enabled using a +special configure option, @file{--enable-encryption}, +when you build @sc{cvs}. + +@item -z @var{gzip-level} +@cindex Compression +@cindex Gzip +Set the compression level. Only has an effect on the +@sc{cvs} client. + +@end table + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Common options +@appendixsec Common command options +@cindex Common options +@cindex Right-hand options + +This section describes the @samp{command_options} that +are available across several @sc{cvs} commands. These +options are always given to the right of +@samp{cvs_command}. Not all +commands support all of these options; each option is +only supported for commands where it makes sense. +However, when a command has one of these options you +can almost always count on the same behavior of the +option as in other commands. (Other command options, +which are listed with the individual commands, may have +different behavior from one @sc{cvs} command to the other). + +@strong{Warning:} the @samp{history} command is an exception; it supports +many options that conflict even with these standard options. + +@table @code +@cindex Dates +@cindex Time +@cindex Specifying dates +@item -D @var{date_spec} +Use the most recent revision no later than @var{date_spec}. +@var{date_spec} is a single argument, a date description +specifying a date in the past. + +The specification is @dfn{sticky} when you use it to make a +private copy of a source file; that is, when you get a working +file using @samp{-D}, @sc{cvs} records the date you specified, so that +further updates in the same directory will use the same date +(for more information on sticky tags/dates, @pxref{Sticky tags}). + +@samp{-D} is available with the @code{checkout}, +@code{diff}, @code{export}, @code{history}, +@code{rdiff}, @code{rtag}, and @code{update} commands. +(The @code{history} command uses this option in a +slightly different way; @pxref{history options}). + +@c What other formats should we accept? I don't want +@c to start accepting a whole mess of non-standard +@c new formats (there are a lot which are in wide use in +@c one context or another), but practicality does +@c dictate some level of flexibility. +@c * POSIX.2 (e.g. touch, ls output, date) and other +@c POSIX and/or de facto unix standards (e.g. at). The +@c practice here is too inconsistent to be of any use. +@c * VMS dates. This is not a formal standard, but +@c there is a published specification (see SYS$ASCTIM +@c and SYS$BINTIM in the _VMS System Services Reference +@c Manual_), it is implemented consistently in VMS +@c utilities, and VMS users will expect CVS running on +@c VMS to support this format (and if we're going to do +@c that, better to make CVS support it on all +@c platforms. Maybe). +@c +@c NOTE: The tar manual has some documentation for +@c getdate.y (just for our info; we don't want to +@c attempt to document all the formats accepted by +@c getdate.y). +@c +@c One more note: In output, CVS should consistently +@c use one date format, and that format should be one that +@c it accepts in input as well. The former isn't +@c really true (see survey below), and I'm not +@c sure that either of those formats is accepted in +@c input. +@c +@c cvs log +@c current 1996/01/02 13:45:31 +@c Internet 02 Jan 1996 13:45:31 UT +@c ISO 1996-01-02 13:45:31 +@c cvs ann +@c current 02-Jan-96 +@c Internet-like 02 Jan 96 +@c ISO 96-01-02 +@c cvs status +@c current Tue Jun 11 02:54:53 1996 +@c Internet [Tue,] 11 Jun 1996 02:54:53 +@c ISO 1996-06-11 02:54:53 +@c note: date possibly should be omitted entirely for +@c other reasons. +@c cvs editors +@c current Tue Jun 11 02:54:53 1996 GMT +@c cvs history +@c current 06/11 02:54 +0000 +@c any others? +@c There is a good chance the proper solution has to +@c involve at least some level of letting the user +@c decide which format (with the default being the +@c formats CVS has always used; changing these might be +@c _very_ disruptive since scripts may very well be +@c parsing them). +@c +@c Another random bit of prior art concerning dates is +@c the strptime function which takes templates such as +@c "%m/%d/%y", and apparent a variant of getdate() +@c which also honors them. See +@c X/Open CAE Specification, System Interfaces and +@c Headers Issue 4, Version 2 (September 1994), in the +@c entry for getdate() on page 231 + +@cindex Timezone, in input +@cindex Zone, time, in input +A wide variety of date formats are supported by +@sc{cvs}. The most standard ones are ISO8601 (from the +International Standards Organization) and the Internet +e-mail standard (specified in RFC822 as amended by +RFC1123). + +@c Probably should be doing more to spell out just what +@c the rules are, rather than just giving examples. +@c But I want to keep this simple too. +@c So I don't know.... +@c A few specific issues: (1) Maybe should reassure +@c people that years after 2000 +@c work (they are in the testsuite, so they do indeed +@c work). (2) What do two digit years +@c mean? Where do we accept them? (3) Local times can +@c be ambiguous or nonexistent if they fall during the +@c hour when daylight savings time goes into or out of +@c effect. Pretty obscure, so I'm not at all sure we +@c should be documenting the behavior in that case. +ISO8601 dates have many variants but a few examples +are: + +@example +1972-09-24 +1972-09-24 20:05 +@end example +@c I doubt we really accept all ISO8601 format dates +@c (for example, decimal hours like 1972-09-24 20,2) +@c I'm not sure we should, many of them are pretty +@c bizarre and it has lots of gratuitous multiple ways +@c to specify the same thing. + +There are a lot more ISO8601 date formats, and CVS +accepts many of them, but you probably don't want to +hear the @emph{whole} long story :-). + +@c Citing a URL here is kind of problematic given how +@c much they change and people who have old versions of +@c this manual, but in case we want to reinstate an +@c ISO8601 URL, a few are: +@c http://www.saqqara.demon.co.uk/datefmt.htm +@c http://www.cl.cam.ac.uk/~mgk25/iso-time.html +@c Citing some other ISO8601 source is probably even +@c worse :-). + +In addition to the dates allowed in Internet e-mail +itself, @sc{cvs} also allows some of the fields to be +omitted. For example: +@c FIXME: Need to figure out better, and document, +@c what we want to allow the user to omit. +@c NOTE: "omit" does not imply "reorder". +@c FIXME: Need to cite a web page describing how to get +@c RFC's. + +@example +24 Sep 1972 20:05 +24 Sep +@end example + +The date is interpreted as being in the +local timezone, unless a specific timezone is +specified. + +These two date formats are preferred. However, +@sc{cvs} currently accepts a wide variety of other date +formats. They are intentionally not documented here in +any detail, and future versions of @sc{cvs} might not +accept all of them. +@c We should document and testsuite "now" and +@c "yesterday". "now" is mentioned in the FAQ and +@c "yesterday" is mentioned in this document (and the +@c message from "cvs import" suggesting a merge +@c command). What else? Probably some/all of the "3 +@c weeks ago" family. +@c +@c Maybe at +@c some point have CVS start give warnings on "unofficial" +@c formats (many of which might be typos or user +@c misunderstandings, and/or formats people never/rarely +@c use to specify dates)? + +One such format is +@code{@var{month}/@var{day}/@var{year}}. This may +confuse people who are accustomed to having the month +and day in the other order; @samp{1/4/96} is January 4, +not April 1. + +Remember to quote the argument to the @samp{-D} +flag so that your shell doesn't interpret spaces as +argument separators. A command using the @samp{-D} +flag can look like this: + +@example +$ cvs diff -D "1 hour ago" cvs.texinfo +@end example + +@cindex Forcing a tag match +@item -f +When you specify a particular date or tag to @sc{cvs} commands, they +normally ignore files that do not contain the tag (or did not +exist prior to the date) that you specified. Use the @samp{-f} option +if you want files retrieved even when there is no match for the +tag or date. (The most recent revision of the file +will be used). + +Note that even with @samp{-f}, a tag that you specify +must exist (that is, in some file, not necessary in +every file). This is so that @sc{cvs} will continue to +give an error if you mistype a tag name. + +@need 800 +@samp{-f} is available with these commands: +@code{annotate}, @code{checkout}, @code{export}, +@code{rdiff}, @code{rtag}, and @code{update}. + +@strong{Warning:} The @code{commit} and @code{remove} +commands also have a +@samp{-f} option, but it has a different behavior for +those commands. See @ref{commit options}, and +@ref{Removing files}. + +@item -k @var{kflag} +Alter the default processing of keywords. +@xref{Keyword substitution}, for the meaning of +@var{kflag}. Your @var{kflag} specification is +@dfn{sticky} when you use it to create a private copy +of a source file; that is, when you use this option +with the @code{checkout} or @code{update} commands, +@sc{cvs} associates your selected @var{kflag} with the +file, and continues to use it with future update +commands on the same file until you specify otherwise. + +The @samp{-k} option is available with the @code{add}, +@code{checkout}, @code{diff}, @code{import} and +@code{update} commands. + +@item -l +Local; run only in current working directory, rather than +recursing through subdirectories. + +@strong{Warning:} this is not the same +as the overall @samp{cvs -l} option, which you can specify to the +left of a cvs command! + +Available with the following commands: @code{annotate}, @code{checkout}, +@code{commit}, @code{diff}, @code{edit}, @code{editors}, @code{export}, +@code{log}, @code{rdiff}, @code{remove}, @code{rtag}, +@code{status}, @code{tag}, @code{unedit}, @code{update}, @code{watch}, +and @code{watchers}. + +@cindex Editor, avoiding invocation of +@cindex Avoiding editor invocation +@item -m @var{message} +Use @var{message} as log information, instead of +invoking an editor. + +Available with the following commands: @code{add}, +@code{commit} and @code{import}. + +@item -n +Do not run any checkout/commit/tag program. (A program can be +specified to run on each of these activities, in the modules +database (@pxref{modules}); this option bypasses it). + +@strong{Warning:} this is not the same as the overall @samp{cvs -n} +option, which you can specify to the left of a cvs command! + +Available with the @code{checkout}, @code{commit}, @code{export}, +and @code{rtag} commands. + +@item -P +Prune empty directories. See @ref{Removing directories}. + +@item -p +Pipe the files retrieved from the repository to standard output, +rather than writing them in the current directory. Available +with the @code{checkout} and @code{update} commands. + +@item -R +Process directories recursively. This is on by default. + +Available with the following commands: @code{annotate}, @code{checkout}, +@code{commit}, @code{diff}, @code{edit}, @code{editors}, @code{export}, +@code{rdiff}, @code{remove}, @code{rtag}, +@code{status}, @code{tag}, @code{unedit}, @code{update}, @code{watch}, +and @code{watchers}. + +@item -r @var{tag} +@cindex HEAD, special tag +@cindex BASE, special tag +Use the revision specified by the @var{tag} argument instead of the +default @dfn{head} revision. As well as arbitrary tags defined +with the @code{tag} or @code{rtag} command, two special tags are +always available: @samp{HEAD} refers to the most recent version +available in the repository, and @samp{BASE} refers to the +revision you last checked out into the current working directory. + +@c FIXME: What does HEAD really mean? I believe that +@c the current answer is the head of the default branch +@c for all cvs commands except diff. For diff, it +@c seems to be (a) the head of the trunk (or the default +@c branch?) if there is no sticky tag, (b) the head of the +@c branch for the sticky tag, if there is a sticky tag. +@c (b) is ugly as it differs +@c from what HEAD means for other commands, but people +@c and/or scripts are quite possibly used to it. +@c See "head" tests in sanity.sh. +@c Probably the best fix is to introduce two new +@c special tags, ".thead" for the head of the trunk, +@c and ".bhead" for the head of the current branch. +@c Then deprecate HEAD. This has the advantage of +@c not surprising people with a change to HEAD, and a +@c side benefit of also phasing out the poorly-named +@c HEAD (see discussion of reserved tag names in node +@c "Tags"). Of course, .thead and .bhead should be +@c carefully implemented (with the implementation the +@c same for "diff" as for everyone else), test cases +@c written (similar to the ones in "head"), new tests +@c cases written for things like default branches, &c. + +The tag specification is sticky when you use this +@c option +with @code{checkout} or @code{update} to make your own +copy of a file: @sc{cvs} remembers the tag and continues to use it on +future update commands, until you specify otherwise (for more information +on sticky tags/dates, @pxref{Sticky tags}). + +The tag can be either a symbolic or numeric tag, as +described in @ref{Tags}, or the name of a branch, as +described in @ref{Branching and merging}. + +Specifying the @samp{-q} global option along with the +@samp{-r} command option is often useful, to suppress +the warning messages when the @sc{rcs} file +does not contain the specified tag. + +@strong{Warning:} this is not the same as the overall @samp{cvs -r} option, +which you can specify to the left of a @sc{cvs} command! + +@samp{-r} is available with the @code{checkout}, @code{commit}, +@code{diff}, @code{history}, @code{export}, @code{rdiff}, +@code{rtag}, and @code{update} commands. + +@item -W +Specify file names that should be filtered. You can +use this option repeatedly. The spec can be a file +name pattern of the same type that you can specify in +the @file{.cvswrappers} file. +Available with the following commands: @code{import}, +and @code{update}. + +@end table + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node admin +@appendixsec admin---Administration +@cindex Admin (subcommand) + +@itemize @bullet +@item +Requires: repository, working directory. +@item +Changes: repository. +@item +Synonym: rcs +@end itemize + +This is the @sc{cvs} interface to assorted +administrative facilities. Some of them have +questionable usefulness for @sc{cvs} but exist for +historical purposes. Some of the questionable options +are likely to disappear in the future. This command +@emph{does} work recursively, so extreme care should be +used. + +@cindex cvsadmin +On unix, if there is a group named @code{cvsadmin}, +only members of that group can run @code{cvs admin} +(except for the @code{cvs admin -k} command, which can +be run by anybody). This group should exist on the +server, or any system running the non-client/server +@sc{cvs}. To disallow @code{cvs admin} for all users, +create a group with no users in it. On NT, the +@code{cvsadmin} feature does not exist and all users +can run @code{cvs admin}. + +@menu +* admin options:: admin options +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node admin options +@appendixsubsec admin options + +Some of these options have questionable usefulness for +@sc{cvs} but exist for historical purposes. Some even +make it impossible to use @sc{cvs} until you undo the +effect! + +@table @code +@item -A@var{oldfile} +Might not work together with @sc{cvs}. Append the +access list of @var{oldfile} to the access list of the +@sc{rcs} file. + +@item -a@var{logins} +Might not work together with @sc{cvs}. Append the +login names appearing in the comma-separated list +@var{logins} to the access list of the @sc{rcs} file. + +@item -b[@var{rev}] +Set the default branch to @var{rev}. In @sc{cvs}, you +normally do not manipulate default branches; sticky +tags (@pxref{Sticky tags}) are a better way to decide +which branch you want to work on. There is one reason +to run @code{cvs admin -b}: to revert to the vendor's +version when using vendor branches (@pxref{Reverting +local changes}). +There can be no space between @samp{-b} and its argument. +@c Hmm, we don't document the usage where rev is +@c omitted. Maybe that usage can/should be deprecated +@c (and replaced with -bHEAD or something?) (so we can toss +@c the optional argument). Note that -bHEAD does not +@c work, as of 17 Sep 1997, but probably will once "cvs +@c admin" is internal to CVS. + +@cindex Comment leader +@item -c@var{string} +Sets the comment leader to @var{string}. The comment +leader is not used by current versions of @sc{cvs} or +@sc{rcs} 5.7. Therefore, you can almost surely not +worry about it. @xref{Keyword substitution}. + +@item -e[@var{logins}] +Might not work together with @sc{cvs}. Erase the login +names appearing in the comma-separated list +@var{logins} from the access list of the RCS file. If +@var{logins} is omitted, erase the entire access list. +There can be no space between @samp{-e} and its argument. + +@item -I +Run interactively, even if the standard input is not a +terminal. This option does not work with the +client/server @sc{cvs} and is likely to disappear in +a future release of @sc{cvs}. + +@item -i +Useless with @sc{cvs}. This creates and initializes a +new @sc{rcs} file, without depositing a revision. With +@sc{cvs}, add files with the @code{cvs add} command +(@pxref{Adding files}). + +@item -k@var{subst} +Set the default keyword +substitution to @var{subst}. @xref{Keyword +substitution}. Giving an explicit @samp{-k} option to +@code{cvs update}, @code{cvs export}, or @code{cvs +checkout} overrides this default. + +@item -l[@var{rev}] +Lock the revision with number @var{rev}. If a branch +is given, lock the latest revision on that branch. If +@var{rev} is omitted, lock the latest revision on the +default branch. There can be no space between +@samp{-l} and its argument. + +This can be used in conjunction with the +@file{rcslock.pl} script in the @file{contrib} +directory of the @sc{cvs} source distribution to +provide reserved checkouts (where only one user can be +editing a given file at a time). See the comments in +that file for details (and see the @file{README} file +in that directory for disclaimers about the unsupported +nature of contrib). According to comments in that +file, locking must set to strict (which is the default). + +@item -L +Set locking to strict. Strict locking means that the +owner of an RCS file is not exempt from locking for +checkin. For use with @sc{cvs}, strict locking must be +set; see the discussion under the @samp{-l} option above. + +@cindex Changing a log message +@cindex Replacing a log message +@cindex Correcting a log message +@cindex Fixing a log message +@cindex Log message, correcting +@item -m@var{rev}:@var{msg} +Replace the log message of revision @var{rev} with +@var{msg}. + +@c The rcs -M option, to suppress sending mail, has never been +@c documented as a cvs admin option. + +@item -N@var{name}[:[@var{rev}]] +Act like @samp{-n}, except override any previous +assignment of @var{name}. For use with magic branches, +see @ref{Magic branch numbers}. + +@item -n@var{name}[:[@var{rev}]] +Associate the symbolic name @var{name} with the branch +or revision @var{rev}. It is normally better to use +@samp{cvs tag} or @samp{cvs rtag} instead. Delete the +symbolic name if both @samp{:} and @var{rev} are +omitted; otherwise, print an error message if +@var{name} is already associated with another number. +If @var{rev} is symbolic, it is expanded before +association. A @var{rev} consisting of a branch number +followed by a @samp{.} stands for the current latest +revision in the branch. A @samp{:} with an empty +@var{rev} stands for the current latest revision on the +default branch, normally the trunk. For example, +@samp{cvs admin -n@var{name}:} associates @var{name} with the +current latest revision of all the RCS files; +this contrasts with @samp{cvs admin -n@var{name}:$} which +associates @var{name} with the revision numbers +extracted from keyword strings in the corresponding +working files. + +@cindex Deleting revisions +@cindex Outdating revisions +@cindex Saving space +@item -o@var{range} +Deletes (@dfn{outdates}) the revisions given by +@var{range}. + +Note that this command can be quite dangerous unless +you know @emph{exactly} what you are doing (for example +see the warnings below about how the +@var{rev1}:@var{rev2} syntax is confusing). + +If you are short on disc this option might help you. +But think twice before using it---there is no way short +of restoring the latest backup to undo this command! +If you delete different revisions than you planned, +either due to carelessness or (heaven forbid) a CVS +bug, there is no opportunity to correct the error +before the revisions are deleted. It probably would be +a good idea to experiment on a copy of the repository +first. + +Specify @var{range} in one of the following ways: + +@table @code +@item @var{rev1}::@var{rev2} +Collapse all revisions between rev1 and rev2, so that +CVS only stores the differences associated with going +from rev1 to rev2, not intermediate steps. For +example, after @samp{-o 1.3::1.5} one can retrieve +revision 1.3, revision 1.5, or the differences to get +from 1.3 to 1.5, but not the revision 1.4, or the +differences between 1.3 and 1.4. Other examples: +@samp{-o 1.3::1.4} and @samp{-o 1.3::1.3} have no +effect, because there are no intermediate revisions to +remove. + +@item ::@var{rev} +Collapse revisions between the beginning of the branch +containing @var{rev} and @var{rev} itself. The +branchpoint and @var{rev} are left intact. For +example, @samp{-o ::1.3.2.6} deletes revision 1.3.2.1, +revision 1.3.2.5, and everything in between, but leaves +1.3 and 1.3.2.6 intact. + +@item @var{rev}:: +Collapse revisions between @var{rev} and the end of the +branch containing @var{rev}. Revision @var{rev} is +left intact but the head revision is deleted. + +@item @var{rev} +Delete the revision @var{rev}. For example, @samp{-o +1.3} is equivalent to @samp{-o 1.2::1.4}. + +@item @var{rev1}:@var{rev2} +Delete the revisions from @var{rev1} to @var{rev2}, +inclusive, on the same branch. One will not be able to +retrieve @var{rev1} or @var{rev2} or any of the +revisions in between. For example, the command +@samp{cvs admin -oR_1_01:R_1_02 .} is rarely useful. +It means to delete revisions up to, and including, the +tag R_1_02. But beware! If there are files that have not +changed between R_1_02 and R_1_03 the file will have +@emph{the same} numerical revision number assigned to +the tags R_1_02 and R_1_03. So not only will it be +impossible to retrieve R_1_02; R_1_03 will also have to +be restored from the tapes! In most cases you want to +specify @var{rev1}::@var{rev2} instead. + +@item :@var{rev} +Delete revisions from the beginning of the +branch containing @var{rev} up to and including +@var{rev}. + +@item @var{rev}: +Delete revisions from revision @var{rev}, including +@var{rev} itself, to the end of the branch containing +@var{rev}. +@end table + +None of the revisions to be deleted may have +branches or locks. + +If any of the revisions to be deleted have symbolic +names, and one specifies one of the @samp{::} syntaxes, +then @sc{cvs} will give an error and not delete any +revisions. If you really want to delete both the +symbolic names and the revisions, first delete the +symbolic names with @code{cvs tag -d}, then run +@code{cvs admin -o}. If one specifies the +non-@samp{::} syntaxes, then @sc{cvs} will delete the +revisions but leave the symbolic names pointing to +nonexistent revisions. This behavior is preserved for +compatibility with previous versions of @sc{cvs}, but +because it isn't very useful, in the future it may +change to be like the @samp{::} case. + +Due to the way @sc{cvs} handles branches @var{rev} +cannot be specified symbolically if it is a branch. +@xref{Magic branch numbers}, for an explanation. +@c FIXME: is this still true? I suspect not. + +Make sure that no-one has checked out a copy of the +revision you outdate. Strange things will happen if he +starts to edit it and tries to check it back in. For +this reason, this option is not a good way to take back +a bogus commit; commit a new revision undoing the bogus +change instead (@pxref{Merging two revisions}). + +@item -q +Run quietly; do not print diagnostics. + +@item -s@var{state}[:@var{rev}] +Useful with @sc{cvs}. Set the state attribute of the +revision @var{rev} to @var{state}. If @var{rev} is a +branch number, assume the latest revision on that +branch. If @var{rev} is omitted, assume the latest +revision on the default branch. Any identifier is +acceptable for @var{state}. A useful set of states is +@samp{Exp} (for experimental), @samp{Stab} (for +stable), and @samp{Rel} (for released). By default, +the state of a new revision is set to @samp{Exp} when +it is created. The state is visible in the output from +@var{cvs log} (@pxref{log}), and in the +@samp{$@asis{}Log$} and @samp{$@asis{}State$} keywords +(@pxref{Keyword substitution}). Note that @sc{cvs} +uses the @code{dead} state for its own purposes; to +take a file to or from the @code{dead} state use +commands like @code{cvs remove} and @code{cvs add}, not +@code{cvs admin -s}. + +@item -t[@var{file}] +Useful with @sc{cvs}. Write descriptive text from the +contents of the named @var{file} into the RCS file, +deleting the existing text. The @var{file} pathname +may not begin with @samp{-}. The descriptive text can be seen in the +output from @samp{cvs log} (@pxref{log}). +There can be no space between @samp{-t} and its argument. + +If @var{file} is omitted, +obtain the text from standard input, terminated by +end-of-file or by a line containing @samp{.} by itself. +Prompt for the text if interaction is possible; see +@samp{-I}. Reading from standard input does not work +for client/server @sc{cvs} and may change in a future +release of @sc{cvs}. +@c Changing it to doeditor() is the most obvious thing +@c (but with a different syntax, as we would like to +@c phase out optional arguments). I don't know. I'm +@c tempted to say the whole concept is unnecessary. + +@item -t-@var{string} +Similar to @samp{-t@var{file}}. Write descriptive text +from the @var{string} into the @sc{rcs} file, deleting +the existing text. +There can be no space between @samp{-t} and its argument. + +@c The rcs -T option, do not update last-mod time for +@c minor changes, has never been documented as a +@c cvs admin option. + +@item -U +Set locking to non-strict. Non-strict locking means +that the owner of a file need not lock a revision for +checkin. For use with @sc{cvs}, strict locking must be +set; see the discussion under the @samp{-l} option +above. + +@item -u[@var{rev}] +See the option @samp{-l} above, for a discussion of +using this option with @sc{cvs}. Unlock the revision +with number @var{rev}. If a branch is given, unlock +the latest revision on that branch. If @var{rev} is +omitted, remove the latest lock held by the caller. +Normally, only the locker of a revision may unlock it. +Somebody else unlocking a revision breaks the lock. +This causes a mail message to be sent to the original +locker. The message contains a commentary solicited +from the breaker. The commentary is terminated by +end-of-file or by a line containing @code{.} by itself. +There can be no space between @samp{-u} and its argument. +@c In the future "send mail" probably will go via the +@c CVSROOT/notify mechanism. But for now it means +@c whatever it means to "rcs". + +@item -V@var{n} +In previous versions of @sc{cvs}, this option meant to +write an @sc{rcs} file which would be acceptable to +@sc{rcs} version @var{n}, but it is now obsolete and +specifying it will produce an error. +@c Note that -V without an argument has never been +@c documented as a cvs admin option. + +@item -x@var{suffixes} +In previous versions of @sc{cvs}, this was documented +as a way of specifying the names of the @sc{rcs} +files. However, @sc{cvs} has always required that the +@sc{rcs} files used by @sc{cvs} end in @samp{,v}, so +this option has never done anything useful. + +@c The rcs -z option, to specify the timezone, has +@c never been documented as a cvs admin option. +@end table + + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node checkout +@appendixsec checkout---Check out sources for editing +@cindex checkout (subcommand) +@cindex co (subcommand) + +@itemize @bullet +@item +Synopsis: checkout [options] modules@dots{} +@item +Requires: repository. +@item +Changes: working directory. +@item +Synonyms: co, get +@end itemize + +Create or update a working directory containing copies of the +source files specified by @var{modules}. You must execute +@code{checkout} before using most of the other @sc{cvs} +commands, since most of them operate on your working +directory. + +The @var{modules} are either +symbolic names for some +collection of source directories and files, or paths to +directories or files in the repository. The symbolic +names are defined in the @samp{modules} file. +@xref{modules}. +@c Needs an example, particularly of the non-"modules" +@c case but probably of both. + +@c FIXME: this seems like a very odd place to introduce +@c people to how CVS works. The bit about unreserved +@c checkouts is also misleading as it depends on how +@c things are set up. +Depending on the modules you specify, @code{checkout} may +recursively create directories and populate them with +the appropriate source files. You can then edit these +source files at any time (regardless of whether other +software developers are editing their own copies of the +sources); update them to include new changes applied by +others to the source repository; or commit your work as +a permanent change to the source repository. + +Note that @code{checkout} is used to create +directories. The top-level directory created is always +added to the directory where @code{checkout} is +invoked, and usually has the same name as the specified +module. In the case of a module alias, the created +sub-directory may have a different name, but you can be +sure that it will be a sub-directory, and that +@code{checkout} will show the relative path leading to +each file as it is extracted into your private work +area (unless you specify the @samp{-Q} global option). + +The files created by @code{checkout} are created +read-write, unless the @samp{-r} option to @sc{cvs} +(@pxref{Global options}) is specified, the +@code{CVSREAD} environment variable is specified +(@pxref{Environment variables}), or a watch is in +effect for that file (@pxref{Watches}). + +Note that running @code{checkout} on a directory that was already +built by a prior @code{checkout} is also permitted. +This is similar to specifying the @samp{-d} option +to the @code{update} command in the sense that new +directories that have been created in the repository +will appear in your work area. +However, @code{checkout} takes a module name whereas +@code{update} takes a directory name. Also +to use @code{checkout} this way it must be run from the +top level directory (where you originally ran +@code{checkout} from), so before you run +@code{checkout} to update an existing directory, don't +forget to change your directory to the top level +directory. + +For the output produced by the @code{checkout} command +see @ref{update output}. + +@menu +* checkout options:: checkout options +* checkout examples:: checkout examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node checkout options +@appendixsubsec checkout options + +These standard options are supported by @code{checkout} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -D @var{date} +Use the most recent revision no later than @var{date}. +This option is sticky, and implies @samp{-P}. See +@ref{Sticky tags}, for more information on sticky tags/dates. + +@item -f +Only useful with the @samp{-D @var{date}} or @samp{-r +@var{tag}} flags. If no matching revision is found, +retrieve the most recent revision (instead of ignoring +the file). + +@item -k @var{kflag} +Process keywords according to @var{kflag}. See +@ref{Keyword substitution}. +This option is sticky; future updates of +this file in this working directory will use the same +@var{kflag}. The @code{status} command can be viewed +to see the sticky options. See @ref{Invoking CVS}, for +more information on the @code{status} command. + +@item -l +Local; run only in current working directory. + +@item -n +Do not run any checkout program (as specified +with the @samp{-o} option in the modules file; +@pxref{modules}). + +@item -P +Prune empty directories. See @ref{Moving directories}. + +@item -p +Pipe files to the standard output. + +@item -R +Checkout directories recursively. This option is on by default. + +@item -r @var{tag} +Use revision @var{tag}. This option is sticky, and implies @samp{-P}. +See @ref{Sticky tags}, for more information on sticky tags/dates. +@end table + +In addition to those, you can use these special command +options with @code{checkout}: + +@table @code +@item -A +Reset any sticky tags, dates, or @samp{-k} options. +See @ref{Sticky tags}, for more information on sticky tags/dates. + +@item -c +Copy the module file, sorted, to the standard output, +instead of creating or modifying any files or +directories in your working directory. + +@item -d @var{dir} +Create a directory called @var{dir} for the working +files, instead of using the module name. In general, +using this flag is equivalent to using @samp{mkdir +@var{dir}; cd @var{dir}} followed by the checkout +command without the @samp{-d} flag. + +There is an important exception, however. It is very +convenient when checking out a single item to have the +output appear in a directory that doesn't contain empty +intermediate directories. In this case @emph{only}, +CVS tries to ``shorten'' pathnames to avoid those empty +directories. + +For example, given a module @samp{foo} that contains +the file @samp{bar.c}, the command @samp{cvs co -d dir +foo} will create directory @samp{dir} and place +@samp{bar.c} inside. Similarly, given a module +@samp{bar} which has subdirectory @samp{baz} wherein +there is a file @samp{quux.c}, the command @samp{cvs -d +dir co bar/baz} will create directory @samp{dir} and +place @samp{quux.c} inside. + +Using the @samp{-N} flag will defeat this behavior. +Given the same module definitions above, @samp{cvs co +-N -d dir foo} will create directories @samp{dir/foo} +and place @samp{bar.c} inside, while @samp{cvs co -N -d +dir bar/baz} will create directories @samp{dir/bar/baz} +and place @samp{quux.c} inside. + +@item -j @var{tag} +With two @samp{-j} options, merge changes from the +revision specified with the first @samp{-j} option to +the revision specified with the second @samp{j} option, +into the working directory. + +With one @samp{-j} option, merge changes from the +ancestor revision to the revision specified with the +@samp{-j} option, into the working directory. The +ancestor revision is the common ancestor of the +revision which the working directory is based on, and +the revision specified in the @samp{-j} option. + +In addition, each -j option can contain an optional +date specification which, when used with branches, can +limit the chosen revision to one within a specific +date. An optional date is specified by adding a colon +(:) to the tag: +@samp{-j@var{Symbolic_Tag}:@var{Date_Specifier}}. + +@xref{Branching and merging}. + +@item -N +Only useful together with @samp{-d @var{dir}}. With +this option, @sc{cvs} will not ``shorten'' module paths +in your working directory when you check out a single +module. See the @samp{-d} flag for examples and a +discussion. + +@item -s +Like @samp{-c}, but include the status of all modules, +and sort it by the status string. @xref{modules}, for +info about the @samp{-s} option that is used inside the +modules file to set the module status. +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node checkout examples +@appendixsubsec checkout examples + +Get a copy of the module @samp{tc}: + +@example +$ cvs checkout tc +@end example + +Get a copy of the module @samp{tc} as it looked one day +ago: + +@example +$ cvs checkout -D yesterday tc +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node commit +@appendixsec commit---Check files into the repository +@cindex commit (subcommand) + +@itemize @bullet +@item +Synopsis: commit [-lnRf] [-m 'log_message' | +-F file] [-r revision] [files@dots{}] +@item +Requires: working directory, repository. +@item +Changes: repository. +@item +Synonym: ci +@end itemize + +Use @code{commit} when you want to incorporate changes +from your working source files into the source +repository. + +If you don't specify particular files to commit, all of +the files in your working current directory are +examined. @code{commit} is careful to change in the +repository only those files that you have really +changed. By default (or if you explicitly specify the +@samp{-R} option), files in subdirectories are also +examined and committed if they have changed; you can +use the @samp{-l} option to limit @code{commit} to the +current directory only. + +@code{commit} verifies that the selected files are up +to date with the current revisions in the source +repository; it will notify you, and exit without +committing, if any of the specified files must be made +current first with @code{update} (@pxref{update}). +@code{commit} does not call the @code{update} command +for you, but rather leaves that for you to do when the +time is right. + +When all is well, an editor is invoked to allow you to +enter a log message that will be written to one or more +logging programs (@pxref{modules}, and @pxref{loginfo}) +and placed in the @sc{rcs} file inside the +repository. This log message can be retrieved with the +@code{log} command; see @ref{log}. You can specify the +log message on the command line with the @samp{-m +@var{message}} option, and thus avoid the editor invocation, +or use the @samp{-F @var{file}} option to specify +that the argument file contains the log message. + +@menu +* commit options:: commit options +* commit examples:: commit examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node commit options +@appendixsubsec commit options + +These standard options are supported by @code{commit} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -l +Local; run only in current working directory. + +@item -n +Do not run any module program. + +@item -R +Commit directories recursively. This is on by default. + +@item -r @var{revision} +Commit to @var{revision}. @var{revision} must be +either a branch, or a revision on the main trunk that +is higher than any existing revision number +(@pxref{Assigning revisions}). You +cannot commit to a specific revision on a branch. +@c FIXME: Need xref for branch case. +@end table + +@code{commit} also supports these options: + +@table @code +@item -F @var{file} +Read the log message from @var{file}, instead +of invoking an editor. + +@item -f +Note that this is not the standard behavior of +the @samp{-f} option as defined in @ref{Common options}. + +Force @sc{cvs} to commit a new revision even if you haven't +made any changes to the file. If the current revision +of @var{file} is 1.7, then the following two commands +are equivalent: + +@example +$ cvs commit -f @var{file} +$ cvs commit -r 1.8 @var{file} +@end example + +@c This is odd, but it's how CVS has worked for some +@c time. +The @samp{-f} option disables recursion (i.e., it +implies @samp{-l}). To force @sc{cvs} to commit a new +revision for all files in all subdirectories, you must +use @samp{-f -R}. + +@item -m @var{message} +Use @var{message} as the log message, instead of +invoking an editor. +@end table + +@need 2000 +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node commit examples +@appendixsubsec commit examples + +@c FIXME: this material wants to be somewhere +@c in "Branching and merging". + +@appendixsubsubsec Committing to a branch + +You can commit to a branch revision (one that has an +even number of dots) with the @samp{-r} option. To +create a branch revision, use the @samp{-b} option +of the @code{rtag} or @code{tag} commands +(@pxref{Branching and merging}). Then, either @code{checkout} or +@code{update} can be used to base your sources on the +newly created branch. From that point on, all +@code{commit} changes made within these working sources +will be automatically added to a branch revision, +thereby not disturbing main-line development in any +way. For example, if you had to create a patch to the +1.2 version of the product, even though the 2.0 version +is already under development, you might do: + +@example +$ cvs rtag -b -r FCS1_2 FCS1_2_Patch product_module +$ cvs checkout -r FCS1_2_Patch product_module +$ cd product_module +[[ hack away ]] +$ cvs commit +@end example + +@noindent +This works automatically since the @samp{-r} option is +sticky. + +@appendixsubsubsec Creating the branch after editing + +Say you have been working on some extremely +experimental software, based on whatever revision you +happened to checkout last week. If others in your +group would like to work on this software with you, but +without disturbing main-line development, you could +commit your change to a new branch. Others can then +checkout your experimental stuff and utilize the full +benefit of @sc{cvs} conflict resolution. The scenario might +look like: + +@c FIXME: Should we be recommending tagging the branchpoint? +@example +[[ hacked sources are present ]] +$ cvs tag -b EXPR1 +$ cvs update -r EXPR1 +$ cvs commit +@end example + +The @code{update} command will make the @samp{-r +EXPR1} option sticky on all files. Note that your +changes to the files will never be removed by the +@code{update} command. The @code{commit} will +automatically commit to the correct branch, because the +@samp{-r} is sticky. You could also do like this: + +@c FIXME: Should we be recommending tagging the branchpoint? +@example +[[ hacked sources are present ]] +$ cvs tag -b EXPR1 +$ cvs commit -r EXPR1 +@end example + +@noindent +but then, only those files that were changed by you +will have the @samp{-r EXPR1} sticky flag. If you hack +away, and commit without specifying the @samp{-r EXPR1} +flag, some files may accidentally end up on the main +trunk. + +To work with you on the experimental change, others +would simply do + +@example +$ cvs checkout -r EXPR1 whatever_module +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node diff +@appendixsec diff---Show differences between revisions +@cindex diff (subcommand) + +@itemize @bullet +@item +Synopsis: diff [-lR] [format_options] [[-r rev1 | -D date1] [-r rev2 | -D date2]] [files@dots{}] +@item +Requires: working directory, repository. +@item +Changes: nothing. +@end itemize + +The @code{diff} command is used to compare different +revisions of files. The default action is to compare +your working files with the revisions they were based +on, and report any differences that are found. + +If any file names are given, only those files are +compared. If any directories are given, all files +under them will be compared. + +The exit status for diff is different than for other +@sc{cvs} commands; for details @ref{Exit status}. + +@menu +* diff options:: diff options +* diff examples:: diff examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node diff options +@appendixsubsec diff options + +These standard options are supported by @code{diff} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -D @var{date} +Use the most recent revision no later than @var{date}. +See @samp{-r} for how this affects the comparison. + +@item -k @var{kflag} +Process keywords according to @var{kflag}. See +@ref{Keyword substitution}. + +@item -l +Local; run only in current working directory. + +@item -R +Examine directories recursively. This option is on by +default. + +@item -r @var{tag} +Compare with revision @var{tag}. Zero, one or two +@samp{-r} options can be present. With no @samp{-r} +option, the working file will be compared with the +revision it was based on. With one @samp{-r}, that +revision will be compared to your current working file. +With two @samp{-r} options those two revisions will be +compared (and your working file will not affect the +outcome in any way). +@c We should be a lot more explicit, with examples, +@c about the difference between "cvs diff" and "cvs +@c diff -r HEAD". This often confuses new users. + +One or both @samp{-r} options can be replaced by a +@samp{-D @var{date}} option, described above. +@end table + +@c Conceptually, this is a disaster. There are 3 +@c zillion diff formats that we support via the diff +@c library. It is not obvious to me that we should +@c document them all. Maybe just the most common ones +@c like -c and -u, and think about phasing out the +@c obscure ones. +@c FIXCVS: also should be a way to specify an external +@c diff program (which can be different for different +@c file types) and pass through +@c arbitrary options, so that the user can do +@c "--pass=-Z --pass=foo" or something even if CVS +@c doesn't know about the "-Z foo" option to diff. +@c This would fit nicely with deprecating/eliminating +@c the obscure options of the diff library, because it +@c would let people specify an external GNU diff if +@c they are into that sort of thing. +The following options specify the format of the +output. They have the same meaning as in GNU diff. + +@example +-0 -1 -2 -3 -4 -5 -6 -7 -8 -9 +--binary +--brief +--changed-group-format=@var{arg} +-c + -C @var{nlines} + --context[=@var{lines}] +-e --ed +-t --expand-tabs +-f --forward-ed +--horizon-lines=@var{arg} +--ifdef=@var{arg} +-w --ignore-all-space +-B --ignore-blank-lines +-i --ignore-case +-I @var{regexp} + --ignore-matching-lines=@var{regexp} +-h +-b --ignore-space-change +-T --initial-tab +-L @var{label} + --label=@var{label} +--left-column +-d --minimal +-N --new-file +--new-line-format=@var{arg} +--old-line-format=@var{arg} +--paginate +-n --rcs +-s --report-identical-files +-p +--show-c-function +-y --side-by-side +-F @var{regexp} +--show-function-line=@var{regexp} +-H --speed-large-files +--suppress-common-lines +-a --text +--unchanged-group-format=@var{arg} +-u + -U @var{nlines} + --unified[=@var{lines}] +@c FIXCVS: This option is accepted by src/diff.c but +@c not diff/diff.c; it would appear that any attempt to +@c use it would get an error. +-V @var{arg} +-W @var{columns} + --width=@var{columns} +@end example + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node diff examples +@appendixsubsec diff examples + +The following line produces a Unidiff (@samp{-u} flag) +between revision 1.14 and 1.19 of +@file{backend.c}. Due to the @samp{-kk} flag no +keywords are substituted, so differences that only depend +on keyword substitution are ignored. + +@example +$ cvs diff -kk -u -r 1.14 -r 1.19 backend.c +@end example + +Suppose the experimental branch EXPR1 was based on a +set of files tagged RELEASE_1_0. To see what has +happened on that branch, the following can be used: + +@example +$ cvs diff -r RELEASE_1_0 -r EXPR1 +@end example + +A command like this can be used to produce a context +diff between two releases: + +@example +$ cvs diff -c -r RELEASE_1_0 -r RELEASE_1_1 > diffs +@end example + +If you are maintaining ChangeLogs, a command like the following +just before you commit your changes may help you write +the ChangeLog entry. All local modifications that have +not yet been committed will be printed. + +@example +$ cvs diff -u | less +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node export +@appendixsec export---Export sources from CVS, similar to checkout +@cindex export (subcommand) + +@itemize @bullet +@item +Synopsis: export [-flNnR] [-r rev|-D date] [-k subst] [-d dir] module@dots{} +@item +Requires: repository. +@item +Changes: current directory. +@end itemize + +This command is a variant of @code{checkout}; use it +when you want a copy of the source for module without +the @sc{cvs} administrative directories. For example, you +might use @code{export} to prepare source for shipment +off-site. This command requires that you specify a +date or tag (with @samp{-D} or @samp{-r}), so that you +can count on reproducing the source you ship to others. + +One often would like to use @samp{-kv} with @code{cvs +export}. This causes any keywords to be +expanded such that an import done at some other site +will not lose the keyword revision information. But be +aware that doesn't handle an export containing binary +files correctly. Also be aware that after having used +@samp{-kv}, one can no longer use the @code{ident} +command (which is part of the @sc{rcs} suite---see +ident(1)) which looks for keyword strings. If +you want to be able to use @code{ident} you must not +use @samp{-kv}. + +@menu +* export options:: export options +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node export options +@appendixsubsec export options + +These standard options are supported by @code{export} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -D @var{date} +Use the most recent revision no later than @var{date}. + +@item -f +If no matching revision is found, retrieve the most +recent revision (instead of ignoring the file). + +@item -l +Local; run only in current working directory. + +@item -n +Do not run any checkout program. + +@item -R +Export directories recursively. This is on by default. + +@item -r @var{tag} +Use revision @var{tag}. +@end table + +In addition, these options (that are common to +@code{checkout} and @code{export}) are also supported: + +@table @code +@item -d @var{dir} +Create a directory called @var{dir} for the working +files, instead of using the module name. +@xref{checkout options}, for complete details on how +@sc{cvs} handles this flag. + +@item -k @var{subst} +Set keyword expansion mode (@pxref{Substitution modes}). + +@item -N +Only useful together with @samp{-d @var{dir}}. +@xref{checkout options}, for complete details on how +@sc{cvs} handles this flag. +@end table + +@ignore +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@c @node export examples +@appendixsubsec export examples + +Contributed examples are gratefully accepted. +@c -- Examples here!! +@end ignore + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node history +@appendixsec history---Show status of files and users +@cindex history (subcommand) + +@itemize @bullet +@item +Synopsis: history [-report] [-flags] [-options args] [files@dots{}] +@item +Requires: the file @file{$CVSROOT/CVSROOT/history} +@item +Changes: nothing. +@end itemize + +@sc{cvs} can keep a history file that tracks each use of the +@code{checkout}, @code{commit}, @code{rtag}, +@code{update}, and @code{release} commands. You can +use @code{history} to display this information in +various formats. + +Logging must be enabled by creating the file +@file{$CVSROOT/CVSROOT/history}. + +@strong{Warning:} @code{history} uses @samp{-f}, @samp{-l}, +@samp{-n}, and @samp{-p} in ways that conflict with the +normal use inside @sc{cvs} (@pxref{Common options}). + +@menu +* history options:: history options +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node history options +@appendixsubsec history options + +Several options (shown above as @samp{-report}) control what +kind of report is generated: + +@table @code +@item -c +Report on each time commit was used (i.e., each time +the repository was modified). + +@item -e +Everything (all record types). Equivalent to +specifying @samp{-x} with all record types. Of course, +@samp{-e} will also include record types which are +added in a future version of @sc{cvs}; if you are +writing a script which can only handle certain record +types, you'll want to specify @samp{-x}. + +@item -m @var{module} +Report on a particular module. (You can meaningfully +use @samp{-m} more than once on the command line.) + +@item -o +Report on checked-out modules. + +@item -T +Report on all tags. + +@item -x @var{type} +Extract a particular set of record types @var{type} from the @sc{cvs} +history. The types are indicated by single letters, +which you may specify in combination. + +Certain commands have a single record type: + +@table @code +@item F +release +@item O +checkout +@item E +export +@item T +rtag +@end table + +@noindent +One of four record types may result from an update: + +@table @code +@item C +A merge was necessary but collisions were +detected (requiring manual merging). +@item G +A merge was necessary and it succeeded. +@item U +A working file was copied from the repository. +@item W +The working copy of a file was deleted during +update (because it was gone from the repository). +@end table + +@noindent +One of three record types results from commit: + +@table @code +@item A +A file was added for the first time. +@item M +A file was modified. +@item R +A file was removed. +@end table +@end table + +The options shown as @samp{-flags} constrain or expand +the report without requiring option arguments: + +@table @code +@item -a +Show data for all users (the default is to show data +only for the user executing @code{history}). + +@item -l +Show last modification only. + +@item -w +Show only the records for modifications done from the +same working directory where @code{history} is +executing. +@end table + +The options shown as @samp{-options @var{args}} constrain the report +based on an argument: + +@table @code +@item -b @var{str} +Show data back to a record containing the string +@var{str} in either the module name, the file name, or +the repository path. + +@item -D @var{date} +Show data since @var{date}. This is slightly different +from the normal use of @samp{-D @var{date}}, which +selects the newest revision older than @var{date}. + +@item -f @var{file} +Show data for a particular file +(you can specify several @samp{-f} options on the same command line). +This is equivalent to specifying the file on the command line. + +@item -n @var{module} +Show data for a particular module +(you can specify several @samp{-n} options on the same command line). + +@item -p @var{repository} +Show data for a particular source repository (you +can specify several @samp{-p} options on the same command +line). + +@item -r @var{rev} +Show records referring to revisions since the revision +or tag named @var{rev} appears in individual @sc{rcs} +files. Each @sc{rcs} file is searched for the revision or +tag. + +@item -t @var{tag} +Show records since tag @var{tag} was last added to the +history file. This differs from the @samp{-r} flag +above in that it reads only the history file, not the +@sc{rcs} files, and is much faster. + +@item -u @var{name} +Show records for user @var{name}. + +@item -z @var{timezone} +Show times in the selected records using the specified +time zone instead of UTC. +@end table + +@ignore +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@c @node history examples +@appendixsubsec history examples + +Contributed examples will gratefully be accepted. +@c -- Examples here! +@end ignore + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node import +@appendixsec import---Import sources into CVS, using vendor branches +@cindex import (subcommand) + +@c FIXME: This node is way too long for one which has subnodes. + +@itemize @bullet +@item +Synopsis: import [-options] repository vendortag releasetag@dots{} +@item +Requires: Repository, source distribution directory. +@item +Changes: repository. +@end itemize + +Use @code{import} to incorporate an entire source +distribution from an outside source (e.g., a source +vendor) into your source repository directory. You can +use this command both for initial creation of a +repository, and for wholesale updates to the module +from the outside source. @xref{Tracking sources}, for +a discussion on this subject. + +The @var{repository} argument gives a directory name +(or a path to a directory) under the @sc{cvs} root directory +for repositories; if the directory did not exist, +import creates it. + +When you use import for updates to source that has been +modified in your source repository (since a prior +import), it will notify you of any files that conflict +in the two branches of development; use @samp{checkout +-j} to reconcile the differences, as import instructs +you to do. + +If @sc{cvs} decides a file should be ignored +(@pxref{cvsignore}), it does not import it and prints +@samp{I } followed by the filename (@pxref{import output}, for a +complete description of the output). + +If the file @file{$CVSROOT/CVSROOT/cvswrappers} exists, +any file whose names match the specifications in that +file will be treated as packages and the appropriate +filtering will be performed on the file/directory +before being imported. @xref{Wrappers}. + +The outside source is saved in a first-level +branch, by default 1.1.1. Updates are leaves of this +branch; for example, files from the first imported +collection of source will be revision 1.1.1.1, then +files from the first imported update will be revision +1.1.1.2, and so on. + +At least three arguments are required. +@var{repository} is needed to identify the collection +of source. @var{vendortag} is a tag for the entire +branch (e.g., for 1.1.1). You must also specify at +least one @var{releasetag} to identify the files at +the leaves created each time you execute @code{import}. + +@c I'm not completely sure this belongs here. But +@c we need to say it _somewhere_ reasonably obvious; it +@c is a common misconception among people first learning CVS +Note that @code{import} does @emph{not} change the +directory in which you invoke it. In particular, it +does not set up that directory as a @sc{cvs} working +directory; if you want to work with the sources import +them first and then check them out into a different +directory (@pxref{Getting the source}). + +@menu +* import options:: import options +* import output:: import output +* import examples:: import examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node import options +@appendixsubsec import options + +This standard option is supported by @code{import} +(@pxref{Common options}, for a complete description): + +@table @code +@item -m @var{message} +Use @var{message} as log information, instead of +invoking an editor. +@end table + +There are the following additional special options. + +@table @code +@item -b @var{branch} +See @ref{Multiple vendor branches}. + +@item -k @var{subst} +Indicate the keyword expansion mode desired. This +setting will apply to all files created during the +import, but not to any files that previously existed in +the repository. See @ref{Substitution modes}, for a +list of valid @samp{-k} settings. + +@item -I @var{name} +Specify file names that should be ignored during +import. You can use this option repeatedly. To avoid +ignoring any files at all (even those ignored by +default), specify `-I !'. + +@var{name} can be a file name pattern of the same type +that you can specify in the @file{.cvsignore} file. +@xref{cvsignore}. +@c -- Is this really true? + +@item -W @var{spec} +Specify file names that should be filtered during +import. You can use this option repeatedly. + +@var{spec} can be a file name pattern of the same type +that you can specify in the @file{.cvswrappers} +file. @xref{Wrappers}. +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node import output +@appendixsubsec import output + +@code{import} keeps you informed of its progress by printing a line +for each file, preceded by one character indicating the status of the file: + +@table @code +@item U @var{file} +The file already exists in the repository and has not been locally +modified; a new revision has been created (if necessary). + +@item N @var{file} +The file is a new file which has been added to the repository. + +@item C @var{file} +The file already exists in the repository but has been locally modified; +you will have to merge the changes. + +@item I @var{file} +The file is being ignored (@pxref{cvsignore}). + +@cindex Symbolic link, importing +@cindex Link, symbolic, importing +@c FIXME: also (somewhere else) probably +@c should be documenting what happens if you "cvs add" +@c a symbolic link. Also maybe what happens if +@c you manually create symbolic links within the +@c repository (? - not sure why we'd want to suggest +@c doing that). +@item L @var{file} +The file is a symbolic link; @code{cvs import} ignores symbolic links. +People periodically suggest that this behavior should +be changed, but if there is a consensus on what it +should be changed to, it doesn't seem to be apparent. +(Various options in the @file{modules} file can be used +to recreate symbolic links on checkout, update, etc.; +@pxref{modules}.) +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node import examples +@appendixsubsec import examples + +See @ref{Tracking sources}, and @ref{From files}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node log +@appendixsec log---Print out log information for files +@cindex log (subcommand) + +@itemize @bullet +@item +Synopsis: log [options] [files@dots{}] +@item +Requires: repository, working directory. +@item +Changes: nothing. +@end itemize + +Display log information for files. @code{log} used to +call the @sc{rcs} utility @code{rlog}. Although this +is no longer true in the current sources, this history +determines the format of the output and the options, +which are not quite in the style of the other @sc{cvs} +commands. + +@cindex Timezone, in output +@cindex Zone, time, in output +@c Kind of a funny place to document the timezone used +@c in output from commands other than @code{log}. +@c There is also more we need to say about this, +@c including what happens in a client/server environment. +The output includes the location of the @sc{rcs} file, +the @dfn{head} revision (the latest revision on the +trunk), all symbolic names (tags) and some other +things. For each revision, the revision number, the +author, the number of lines added/deleted and the log +message are printed. All times are displayed in +Coordinated Universal Time (UTC). (Other parts of +@sc{cvs} print times in the local timezone). +@c FIXCVS: need a better way to control the timezone +@c used in output. Previous/current versions of CVS did/do +@c sometimes support -z in RCSINIT, and/or an +@c undocumented (except by reference to 'rlog') -z option +@c to cvs log, but this has not been a consistent, +@c documented feature. Perhaps a new global option, +@c where LT means the client's timezone, which the +@c client then communicates to the server, is the +@c right solution. + +@strong{Warning:} @code{log} uses @samp{-R} in a way that conflicts +with the normal use inside @sc{cvs} (@pxref{Common options}). + +@menu +* log options:: log options +* log examples:: log examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node log options +@appendixsubsec log options + +By default, @code{log} prints all information that is +available. All other options restrict the output. + +@table @code +@item -b +Print information about the revisions on the default +branch, normally the highest branch on the trunk. + +@item -d @var{dates} +Print information about revisions with a checkin +date/time in the range given by the +semicolon-separated list of dates. The date formats +accepted are those accepted by the @samp{-D} option to +many other @sc{cvs} commands (@pxref{Common options}). +Dates can be combined into ranges as follows: + +@c Should we be thinking about accepting ISO8601 +@c ranges? For example "1972-09-10/1972-09-12". +@table @code +@item @var{d1}<@var{d2} +@itemx @var{d2}>@var{d1} +Select the revisions that were deposited between +@var{d1} and @var{d2}. + +@item <@var{d} +@itemx @var{d}> +Select all revisions dated @var{d} or earlier. + +@item @var{d}< +@itemx >@var{d} +Select all revisions dated @var{d} or later. + +@item @var{d} +Select the single, latest revision dated @var{d} or +earlier. +@end table + +The @samp{>} or @samp{<} characters may be followed by +@samp{=} to indicate an inclusive range rather than an +exclusive one. + +Note that the separator is a semicolon (;). + +@item -h +Print only the name of the @sc{rcs} file, name +of the file in the working directory, head, +default branch, access list, locks, symbolic names, and +suffix. + +@item -l +Local; run only in current working directory. (Default +is to run recursively). + +@item -N +Do not print the list of tags for this file. This +option can be very useful when your site uses a lot of +tags, so rather than "more"'ing over 3 pages of tag +information, the log information is presented without +tags at all. + +@item -R +Print only the name of the @sc{rcs} file. + +@c Note that using a bare revision (in addition to not +@c being explicitly documented here) is potentially +@c confusing; it shows the log message to get from the +@c previous revision to that revision. "-r1.3 -r1.6" +@c (equivalent to "-r1.3,1.6") is even worse; it +@c prints the messages to get from 1.2 to 1.3 and 1.5 +@c to 1.6. By analogy with "cvs diff", users might +@c expect that it is more like specifying a range. +@c It is not 100% clear to me how much of this should +@c be documented (for example, multiple -r options +@c perhaps could/should be deprecated given the false +@c analogy with "cvs diff"). +@c In general, this section should be rewritten to talk +@c about messages to get from revision rev1 to rev2, +@c rather than messages for revision rev2 (that is, the +@c messages are associated with a change not a static +@c revision and failing to make this distinction causes +@c much confusion). +@item -r@var{revisions} +Print information about revisions given in the +comma-separated list @var{revisions} of revisions and +ranges. The following table explains the available +range formats: + +@table @code +@item @var{rev1}:@var{rev2} +Revisions @var{rev1} to @var{rev2} (which must be on +the same branch). + +@item :@var{rev} +Revisions from the beginning of the branch up to +and including @var{rev}. + +@item @var{rev}: +Revisions starting with @var{rev} to the end of the +branch containing @var{rev}. + +@item @var{branch} +An argument that is a branch means all revisions on +that branch. + +@item @var{branch1}:@var{branch2} +A range of branches means all revisions +on the branches in that range. + +@item @var{branch}. +The latest revision in @var{branch}. +@end table + +A bare @samp{-r} with no revisions means the latest +revision on the default branch, normally the trunk. +There can be no space between the @samp{-r} option and +its argument. + +@item -s @var{states} +Print information about revisions whose state +attributes match one of the states given in the +comma-separated list @var{states}. + +@item -t +Print the same as @samp{-h}, plus the descriptive text. + +@item -w@var{logins} +Print information about revisions checked in by users +with login names appearing in the comma-separated list +@var{logins}. If @var{logins} is omitted, the user's +login is assumed. There can be no space between the +@samp{-w} option and its argument. +@end table + +@code{log} prints the intersection of the revisions +selected with the options @samp{-d}, @samp{-s}, and +@samp{-w}, intersected with the union of the revisions +selected by @samp{-b} and @samp{-r}. + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node log examples +@appendixsubsec log examples + +Contributed examples are gratefully accepted. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node rdiff +@appendixsec rdiff---'patch' format diffs between releases +@cindex rdiff (subcommand) + +@itemize @bullet +@item +rdiff [-flags] [-V vn] [-r t|-D d [-r t2|-D d2]] modules@dots{} +@item +Requires: repository. +@item +Changes: nothing. +@item +Synonym: patch +@end itemize + +Builds a Larry Wall format patch(1) file between two +releases, that can be fed directly into the @code{patch} +program to bring an old release up-to-date with the new +release. (This is one of the few @sc{cvs} commands that +operates directly from the repository, and doesn't +require a prior checkout.) The diff output is sent to +the standard output device. + +You can specify (using the standard @samp{-r} and +@samp{-D} options) any combination of one or two +revisions or dates. If only one revision or date is +specified, the patch file reflects differences between +that revision or date and the current head revisions in +the @sc{rcs} file. + +Note that if the software release affected is contained +in more than one directory, then it may be necessary to +specify the @samp{-p} option to the @code{patch} command when +patching the old sources, so that @code{patch} is able to find +the files that are located in other directories. + +@menu +* rdiff options:: rdiff options +* rdiff examples:: rdiff examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node rdiff options +@appendixsubsec rdiff options + +These standard options are supported by @code{rdiff} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -D @var{date} +Use the most recent revision no later than @var{date}. + +@item -f +If no matching revision is found, retrieve the most +recent revision (instead of ignoring the file). + +@item -l +Local; don't descend subdirectories. + +@item -R +Examine directories recursively. This option is on by default. + +@item -r @var{tag} +Use revision @var{tag}. +@end table + +In addition to the above, these options are available: + +@table @code +@item -c +Use the context diff format. This is the default format. + +@item -s +Create a summary change report instead of a patch. The +summary includes information about files that were +changed or added between the releases. It is sent to +the standard output device. This is useful for finding +out, for example, which files have changed between two +dates or revisions. + +@item -t +A diff of the top two revisions is sent to the standard +output device. This is most useful for seeing what the +last change to a file was. + +@item -u +Use the unidiff format for the context diffs. +Remember that old versions +of the @code{patch} program can't handle the unidiff +format, so if you plan to post this patch to the net +you should probably not use @samp{-u}. + +@item -V @var{vn} +Expand keywords according to the rules current in +@sc{rcs} version @var{vn} (the expansion format changed with +@sc{rcs} version 5). Note that this option is no +longer accepted. CVS will always expand keywords the +way that @sc{rcs} version 5 does. +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node rdiff examples +@appendixsubsec rdiff examples + +Suppose you receive mail from @t{foo@@example.net} asking for an +update from release 1.2 to 1.4 of the tc compiler. You +have no such patches on hand, but with @sc{cvs} that can +easily be fixed with a command such as this: + +@example +$ cvs rdiff -c -r FOO1_2 -r FOO1_4 tc | \ +$$ Mail -s 'The patches you asked for' foo@@example.net +@end example + +Suppose you have made release 1.3, and forked a branch +called @samp{R_1_3fix} for bugfixes. @samp{R_1_3_1} +corresponds to release 1.3.1, which was made some time +ago. Now, you want to see how much development has been +done on the branch. This command can be used: + +@example +$ cvs patch -s -r R_1_3_1 -r R_1_3fix module-name +cvs rdiff: Diffing module-name +File ChangeLog,v changed from revision 1.52.2.5 to 1.52.2.6 +File foo.c,v changed from revision 1.52.2.3 to 1.52.2.4 +File bar.h,v changed from revision 1.29.2.1 to 1.2 +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node release +@appendixsec release---Indicate that a Module is no longer in use +@cindex release (subcommand) + +@itemize @bullet +@item +release [-d] directories@dots{} +@item +Requires: Working directory. +@item +Changes: Working directory, history log. +@end itemize + +This command is meant to safely cancel the effect of +@samp{cvs checkout}. Since @sc{cvs} doesn't lock files, it +isn't strictly necessary to use this command. You can +always simply delete your working directory, if you +like; but you risk losing changes you may have +forgotten, and you leave no trace in the @sc{cvs} history +file (@pxref{history file}) that you've abandoned your +checkout. + +Use @samp{cvs release} to avoid these problems. This +command checks that no uncommitted changes are +present; that you are executing it from immediately +above a @sc{cvs} working directory; and that the repository +recorded for your files is the same as the repository +defined in the module database. + +If all these conditions are true, @samp{cvs release} +leaves a record of its execution (attesting to your +intentionally abandoning your checkout) in the @sc{cvs} +history log. + +@menu +* release options:: release options +* release output:: release output +* release examples:: release examples +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node release options +@appendixsubsec release options + +The @code{release} command supports one command option: + +@table @code +@item -d +Delete your working copy of the file if the release +succeeds. If this flag is not given your files will +remain in your working directory. + +@strong{Warning:} The @code{release} command deletes +all directories and files recursively. This +has the very serious side-effect that any directory +that you have created inside your checked-out sources, +and not added to the repository (using the @code{add} +command; @pxref{Adding files}) will be silently deleted---even +if it is non-empty! +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node release output +@appendixsubsec release output + +Before @code{release} releases your sources it will +print a one-line message for any file that is not +up-to-date. + +@strong{Warning:} Any new directories that you have +created, but not added to the @sc{cvs} directory hierarchy +with the @code{add} command (@pxref{Adding files}) will be +silently ignored (and deleted, if @samp{-d} is +specified), even if they contain files. +@c FIXCVS: This is a bug. But is it true? I think +@c maybe they print "? dir" now. + +@table @code +@item U @var{file} +@itemx P @var{file} +There exists a newer revision of this file in the +repository, and you have not modified your local copy +of the file (@samp{U} and @samp{P} mean the same thing). + +@item A @var{file} +The file has been added to your private copy of the +sources, but has not yet been committed to the +repository. If you delete your copy of the sources +this file will be lost. + +@item R @var{file} +The file has been removed from your private copy of the +sources, but has not yet been removed from the +repository, since you have not yet committed the +removal. @xref{commit}. + +@item M @var{file} +The file is modified in your working directory. There +might also be a newer revision inside the repository. + +@item ? @var{file} +@var{file} is in your working directory, but does not +correspond to anything in the source repository, and is +not in the list of files for @sc{cvs} to ignore (see the +description of the @samp{-I} option, and +@pxref{cvsignore}). If you remove your working +sources, this file will be lost. +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node release examples +@appendixsubsec release examples + +Release the @file{tc} directory, and delete your local working copy +of the files. + +@example +$ cd .. # @r{You must stand immediately above the} + # @r{sources when you issue @samp{cvs release}.} +$ cvs release -d tc +You have [0] altered files in this repository. +Are you sure you want to release (and delete) directory `tc': y +$ +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node update +@appendixsec update---Bring work tree in sync with repository +@cindex update (subcommand) + +@itemize @bullet +@item +update [-AdflPpR] [-d] [-r tag|-D date] files@dots{} +@item +Requires: repository, working directory. +@item +Changes: working directory. +@end itemize + +After you've run checkout to create your private copy +of source from the common repository, other developers +will continue changing the central source. From time +to time, when it is convenient in your development +process, you can use the @code{update} command from +within your working directory to reconcile your work +with any revisions applied to the source repository +since your last checkout or update. + +@menu +* update options:: update options +* update output:: update output +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node update options +@appendixsubsec update options + +These standard options are available with @code{update} +(@pxref{Common options}, for a complete description of +them): + +@table @code +@item -D date +Use the most recent revision no later than @var{date}. +This option is sticky, and implies @samp{-P}. +See @ref{Sticky tags}, for more information on sticky tags/dates. + +@item -f +Only useful with the @samp{-D @var{date}} or @samp{-r +@var{tag}} flags. If no matching revision is found, +retrieve the most recent revision (instead of ignoring +the file). + +@item -k @var{kflag} +Process keywords according to @var{kflag}. See +@ref{Keyword substitution}. +This option is sticky; future updates of +this file in this working directory will use the same +@var{kflag}. The @code{status} command can be viewed +to see the sticky options. See @ref{Invoking CVS}, for +more information on the @code{status} command. + +@item -l +Local; run only in current working directory. @xref{Recursive behavior}. + +@item -P +Prune empty directories. See @ref{Moving directories}. + +@item -p +Pipe files to the standard output. + +@item -R +Update directories recursively (default). @xref{Recursive +behavior}. + +@item -r rev +Retrieve revision/tag @var{rev}. This option is sticky, +and implies @samp{-P}. +See @ref{Sticky tags}, for more information on sticky tags/dates. +@end table + +@need 800 +These special options are also available with +@code{update}. + +@table @code +@item -A +Reset any sticky tags, dates, or @samp{-k} options. +See @ref{Sticky tags}, for more information on sticky tags/dates. + +@item -C +Overwrite locally modified files with clean copies from +the repository (the modified file is saved in +@file{.#@var{file}.@var{revision}}, however). + +@item -d +Create any directories that exist in the repository if +they're missing from the working directory. Normally, +@code{update} acts only on directories and files that +were already enrolled in your working directory. + +This is useful for updating directories that were +created in the repository since the initial checkout; +but it has an unfortunate side effect. If you +deliberately avoided certain directories in the +repository when you created your working directory +(either through use of a module name or by listing +explicitly the files and directories you wanted on the +command line), then updating with @samp{-d} will create +those directories, which may not be what you want. + +@item -I @var{name} +Ignore files whose names match @var{name} (in your +working directory) during the update. You can specify +@samp{-I} more than once on the command line to specify +several files to ignore. Use @samp{-I !} to avoid +ignoring any files at all. @xref{cvsignore}, for other +ways to make @sc{cvs} ignore some files. + +@item -W@var{spec} +Specify file names that should be filtered during +update. You can use this option repeatedly. + +@var{spec} can be a file name pattern of the same type +that you can specify in the @file{.cvswrappers} +file. @xref{Wrappers}. + +@item -j@var{revision} +With two @samp{-j} options, merge changes from the +revision specified with the first @samp{-j} option to +the revision specified with the second @samp{j} option, +into the working directory. + +With one @samp{-j} option, merge changes from the +ancestor revision to the revision specified with the +@samp{-j} option, into the working directory. The +ancestor revision is the common ancestor of the +revision which the working directory is based on, and +the revision specified in the @samp{-j} option. + +In addition, each @samp{-j} option can contain an optional +date specification which, when used with branches, can +limit the chosen revision to one within a specific +date. An optional date is specified by adding a colon +(:) to the tag: +@samp{-j@var{Symbolic_Tag}:@var{Date_Specifier}}. + +@xref{Branching and merging}. + +@end table + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node update output +@appendixsubsec update output + +@code{update} and @code{checkout} keep you informed of +their progress by printing a line for each file, preceded +by one character indicating the status of the file: + +@table @code +@item U @var{file} +The file was brought up to date with respect to the +repository. This is done for any file that exists in +the repository but not in your source, and for files +that you haven't changed but are not the most recent +versions available in the repository. + +@item P @var{file} +Like @samp{U}, but the @sc{cvs} server sends a patch +instead of an entire file. These two things accomplish +the same thing. + +@item A @var{file} +The file has been added to your private copy of the +sources, and will be added to the source repository +when you run @code{commit} on the file. This is a +reminder to you that the file needs to be committed. + +@item R @var{file} +The file has been removed from your private copy of the +sources, and will be removed from the source repository +when you run @code{commit} on the file. This is a +reminder to you that the file needs to be committed. + +@item M @var{file} +The file is modified in your working directory. + +@samp{M} can indicate one of two states for a file +you're working on: either there were no modifications +to the same file in the repository, so that your file +remains as you last saw it; or there were modifications +in the repository as well as in your copy, but they +were merged successfully, without conflict, in your +working directory. + +@sc{cvs} will print some messages if it merges your work, +and a backup copy of your working file (as it looked +before you ran @code{update}) will be made. The exact +name of that file is printed while @code{update} runs. + +@item C @var{file} +@cindex .# files +@cindex __ files (VMS) +A conflict was detected while trying to merge your +changes to @var{file} with changes from the source +repository. @var{file} (the copy in your working +directory) is now the result of attempting to merge +the two revisions; an unmodified copy of your file +is also in your working directory, with the name +@file{.#@var{file}.@var{revision}} where @var{revision} +is the revision that your modified file started +from. Resolve the conflict as described in +@ref{Conflicts example}. +@c "some systems" as in out-of-the-box OSes? Not as +@c far as I know. We need to advise sysadmins as well +@c as users how to set up this kind of purge, if that is +@c what they want. +@c We also might want to think about cleaner solutions, +@c like having CVS remove the .# file once the conflict +@c has been resolved or something like that. +(Note that some systems automatically purge +files that begin with @file{.#} if they have not been +accessed for a few days. If you intend to keep a copy +of your original file, it is a very good idea to rename +it.) Under @sc{vms}, the file name starts with +@file{__} rather than @file{.#}. + +@item ? @var{file} +@var{file} is in your working directory, but does not +correspond to anything in the source repository, and is +not in the list of files for @sc{cvs} to ignore (see the +description of the @samp{-I} option, and +@pxref{cvsignore}). +@end table + +@node Invoking CVS +@appendix Quick reference to CVS commands +@cindex Command reference +@cindex Reference, commands +@cindex Invoking CVS + +This appendix describes how to invoke @sc{cvs}, with +references to where each command or feature is +described in detail. For other references run the +@code{cvs --help} command, or see @ref{Index}. + +A @sc{cvs} command looks like: + +@example +cvs [ @var{global_options} ] @var{command} [ @var{command_options} ] [ @var{command_args} ] +@end example + +Global options: + +@table @code +@item --allow-root=@var{rootdir} +Specify legal @sc{cvsroot} directory (server only) (not +in @sc{cvs} 1.9 and older). See @ref{Password +authentication server}. + +@item -a +Authenticate all communication (client only) (not in @sc{cvs} +1.9 and older). See @ref{Global options}. + +@item -b +Specify RCS location (@sc{cvs} 1.9 and older). See +@ref{Global options}. + +@item -d @var{root} +Specify the @sc{cvsroot}. See @ref{Repository}. + +@item -e @var{editor} +Edit messages with @var{editor}. See @ref{Committing +your changes}. + +@item -f +Do not read the @file{~/.cvsrc} file. See @ref{Global +options}. + +@item -H +@itemx --help +Print a help message. See @ref{Global options}. + +@item -l +Do not log in @file{$CVSROOT/CVSROOT/history} file. See @ref{Global +options}. + +@item -n +Do not change any files. See @ref{Global options}. + +@item -Q +Be really quiet. See @ref{Global options}. + +@item -q +Be somewhat quiet. See @ref{Global options}. + +@item -r +Make new working files read-only. See @ref{Global options}. + +@item -s @var{variable}=@var{value} +Set a user variable. See @ref{Variables}. + +@item -T @var{tempdir} +Put temporary files in @var{tempdir}. See @ref{Global +options}. + +@item -t +Trace @sc{cvs} execution. See @ref{Global options}. + +@item -v +@item --version +Display version and copyright information for @sc{cvs}. + +@item -w +Make new working files read-write. See @ref{Global +options}. + +@item -x +Encrypt all communication (client only). See +@ref{Global options}. + +@item -z @var{gzip-level} +@cindex Compression +@cindex Gzip +Set the compression level (client only). +@c FIXME: what are the valid values for gzip-level. +@c And shouldn't this be documented in at least a +@c little bit of detail somewhere? + +@end table + +Keyword expansion modes (@pxref{Substitution modes}): + +@example +-kkv $@asis{}Id: file1,v 1.1 1993/12/09 03:21:13 joe Exp $ +-kkvl $@asis{}Id: file1,v 1.1 1993/12/09 03:21:13 joe Exp harry $ +-kk $@asis{}Id$ +-kv file1,v 1.1 1993/12/09 03:21:13 joe Exp +-ko @i{no expansion} +-kb @i{no expansion, file is binary} +@end example + +Keywords (@pxref{Keyword list}): + +@example +$@asis{}Author: joe $ +$@asis{}Date: 1993/12/09 03:21:13 $ +$@asis{}Header: /home/files/file1,v 1.1 1993/12/09 03:21:13 joe Exp harry $ +$@asis{}Id: file1,v 1.1 1993/12/09 03:21:13 joe Exp harry $ +$@asis{}Locker: harry $ +$@asis{}Name: snapshot_1_14 $ +$@asis{}RCSfile: file1,v $ +$@asis{}Revision: 1.1 $ +$@asis{}Source: /home/files/file1,v $ +$@asis{}State: Exp $ +$@asis{}Log: file1,v $ +Revision 1.1 1993/12/09 03:30:17 joe +Initial revision + +@end example + +@c The idea behind this table is that we want each item +@c to be a sentence or two at most. Preferably a +@c single line. +@c +@c In some cases refs to "foo options" are just to get +@c this thing written quickly, not because the "foo +@c options" node is really the best place to point. +Commands, command options, and command arguments: + +@table @code +@item add [@var{options}] [@var{files}@dots{}] +Add a new file/directory. See @ref{Adding files}. + +@table @code +@item -k @var{kflag} +Set keyword expansion. + +@item -m @var{msg} +Set file description. +@end table + +@item admin [@var{options}] [@var{files}@dots{}] +Administration of history files in the repository. See +@ref{admin}. +@c This list omits those options which are not +@c documented as being useful with CVS. That might be +@c a mistake... + +@table @code +@item -b[@var{rev}] +Set default branch. See @ref{Reverting local changes}. + +@item -c@var{string} +Set comment leader. + +@item -k@var{subst} +Set keyword substitution. See @ref{Keyword +substitution}. + +@item -l[@var{rev}] +Lock revision @var{rev}, or latest revision. + +@item -m@var{rev}:@var{msg} +Replace the log message of revision @var{rev} with +@var{msg}. + +@item -o@var{range} +Delete revisions from the repository. See +@ref{admin options}. + +@item -q +Run quietly; do not print diagnostics. + +@item -s@var{state}[:@var{rev}] +Set the state. + +@c Does not work for client/server CVS +@item -t +Set file description from standard input. + +@item -t@var{file} +Set file description from @var{file}. + +@item -t-@var{string} +Set file description to @var{string}. + +@item -u[@var{rev}] +Unlock revision @var{rev}, or latest revision. +@end table + +@item annotate [@var{options}] [@var{files}@dots{}] +Show last revision where each line was modified. See +@ref{annotate}. + +@table @code +@item -D @var{date} +Annotate the most recent revision no later than +@var{date}. See @ref{Common options}. + +@item -f +Use head revision if tag/date not found. See +@ref{Common options}. + +@item -l +Local; run only in current working directory. @xref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{tag} +Annotate revision @var{tag}. See @ref{Common options}. +@end table + +@item checkout [@var{options}] @var{modules}@dots{} +Get a copy of the sources. See @ref{checkout}. + +@table @code +@item -A +Reset any sticky tags/date/options. See @ref{Sticky +tags} and @ref{Keyword substitution}. + +@item -c +Output the module database. See @ref{checkout options}. + +@item -D @var{date} +Check out revisions as of @var{date} (is sticky). See +@ref{Common options}. + +@item -d @var{dir} +Check out into @var{dir}. See @ref{checkout options}. + +@item -f +Use head revision if tag/date not found. See +@ref{Common options}. + +@c Probably want to use rev1/rev2 style like for diff +@c -r. Here and in on-line help. +@item -j @var{rev} +Merge in changes. See @ref{checkout options}. + +@item -k @var{kflag} +Use @var{kflag} keyword expansion. See +@ref{Substitution modes}. + +@item -l +Local; run only in current working directory. @xref{Recursive behavior}. + +@item -N +Don't ``shorten'' module paths if -d specified. See +@ref{checkout options}. + +@item -n +Do not run module program (if any). See @ref{checkout options}. + +@item -P +Prune empty directories. See @ref{Moving directories}. + +@item -p +Check out files to standard output (avoids +stickiness). See @ref{checkout options}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{tag} +Checkout revision @var{tag} (is sticky). See @ref{Common options}. + +@item -s +Like -c, but include module status. See @ref{checkout options}. +@end table + +@item commit [@var{options}] [@var{files}@dots{}] +Check changes into the repository. See @ref{commit}. + +@table @code +@item -F @var{file} +Read log message from @var{file}. See @ref{commit options}. + +@item -f +@c What is this "disables recursion"? It is from the +@c on-line help; is it documented in this manual? +Force the file to be committed; disables recursion. +See @ref{commit options}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -m @var{msg} +Use @var{msg} as log message. See @ref{commit options}. + +@item -n +Do not run module program (if any). See @ref{commit options}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{rev} +Commit to @var{rev}. See @ref{commit options}. +@c FIXME: should be dragging over text from +@c commit options, especially if it can be cleaned up +@c and made concise enough. +@end table + +@item diff [@var{options}] [@var{files}@dots{}] +Show differences between revisions. See @ref{diff}. +In addition to the options shown below, accepts a wide +variety of options to control output style, for example +@samp{-c} for context diffs. + +@table @code +@item -D @var{date1} +Diff revision for date against working file. See +@ref{diff options}. + +@item -D @var{date2} +Diff @var{rev1}/@var{date1} against @var{date2}. See +@ref{diff options}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -N +Include diffs for added and removed files. See +@ref{diff options}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{rev1} +Diff revision for @var{rev1} against working file. See +@ref{diff options}. + +@item -r @var{rev2} +Diff @var{rev1}/@var{date1} against @var{rev2}. See @ref{diff options}. +@end table + +@item edit [@var{options}] [@var{files}@dots{}] +Get ready to edit a watched file. See @ref{Editing files}. + +@table @code +@item -a @var{actions} +Specify actions for temporary watch, where +@var{actions} is @code{edit}, @code{unedit}, +@code{commit}, @code{all}, or @code{none}. See +@ref{Editing files}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@item editors [@var{options}] [@var{files}@dots{}] +See who is editing a watched file. See @ref{Watch information}. + +@table @code +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@item export [@var{options}] @var{modules}@dots{} +Export files from CVS. See @ref{export}. + +@table @code +@item -D @var{date} +Check out revisions as of @var{date}. See +@ref{Common options}. + +@item -d @var{dir} +Check out into @var{dir}. See @ref{export options}. + +@item -f +Use head revision if tag/date not found. See +@ref{Common options}. + +@item -k @var{kflag} +Use @var{kflag} keyword expansion. See +@ref{Substitution modes}. + +@item -l +Local; run only in current working directory. @xref{Recursive behavior}. + +@item -N +Don't ``shorten'' module paths if -d specified. See +@ref{export options}. + +@item -n +Do not run module program (if any). See @ref{export options}. + +@item -P +Prune empty directories. See @ref{Moving directories}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{tag} +Checkout revision @var{tag}. See @ref{Common options}. +@end table + +@item history [@var{options}] [@var{files}@dots{}] +Show repository access history. See @ref{history}. + +@table @code +@item -a +All users (default is self). See @ref{history options}. + +@item -b @var{str} +Back to record with @var{str} in module/file/repos +field. See @ref{history options}. + +@item -c +Report on committed (modified) files. See @ref{history options}. + +@item -D @var{date} +Since @var{date}. See @ref{history options}. + +@item -e +Report on all record types. See @ref{history options}. + +@item -l +Last modified (committed or modified report). See @ref{history options}. + +@item -m @var{module} +Report on @var{module} (repeatable). See @ref{history +options}. + +@item -n @var{module} +In @var{module}. See @ref{history options}. + +@item -o +Report on checked out modules. See @ref{history options}. + +@item -r @var{rev} +Since revision @var{rev}. See @ref{history options}. + +@item -T +@c What the @#$@# is a TAG? Same as a tag? This +@c wording is also in the online-line help. +Produce report on all TAGs. See @ref{history options}. + +@item -t @var{tag} +Since tag record placed in history file (by anyone). +See @ref{history options}. + +@item -u @var{user} +For user @var{user} (repeatable). See @ref{history +options}. + +@item -w +Working directory must match. See @ref{history options}. + +@item -x @var{types} +Report on @var{types}, one or more of +@code{TOEFWUCGMAR}. See @ref{history options}. + +@item -z @var{zone} +Output for time zone @var{zone}. See @ref{history +options}. +@end table + +@item import [@var{options}] @var{repository} @var{vendor-tag} @var{release-tags}@dots{} +Import files into CVS, using vendor branches. See +@ref{import}. + +@table @code +@item -b @var{bra} +Import to vendor branch @var{bra}. See +@ref{Multiple vendor branches}. + +@item -d +Use the file's modification time as the time of +import. See @ref{import options}. + +@item -k @var{kflag} +Set default keyword substitution mode. See +@ref{import options}. + +@item -m @var{msg} +Use @var{msg} for log message. See +@ref{import options}. + +@item -I @var{ign} +More files to ignore (! to reset). See +@ref{import options}. + +@item -W @var{spec} +More wrappers. See @ref{import options}. +@end table + +@item init +Create a CVS repository if it doesn't exist. See +@ref{Creating a repository}. + +@item log [@var{options}] [@var{files}@dots{}] +Print out history information for files. See @ref{log}. + +@table @code +@item -b +Only list revisions on the default branch. See @ref{log options}. + +@item -d @var{dates} +Specify dates (@var{d1}<@var{d2} for range, @var{d} for +latest before). See @ref{log options}. + +@item -h +Only print header. See @ref{log options}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -N +Do not list tags. See @ref{log options}. + +@item -R +Only print name of RCS file. See @ref{log options}. + +@item -r@var{revs} +Only list revisions @var{revs}. See @ref{log options}. + +@item -s @var{states} +Only list revisions with specified states. See @ref{log options}. + +@item -t +Only print header and descriptive text. See @ref{log +options}. + +@item -w@var{logins} +Only list revisions checked in by specified logins. See @ref{log options}. +@end table + +@item login +Prompt for password for authenticating server. See +@ref{Password authentication client}. + +@item logout +Remove stored password for authenticating server. See +@ref{Password authentication client}. + +@item rdiff [@var{options}] @var{modules}@dots{} +Show differences between releases. See @ref{rdiff}. + +@table @code +@item -c +Context diff output format (default). See @ref{rdiff options}. + +@item -D @var{date} +Select revisions based on @var{date}. See @ref{Common options}. + +@item -f +Use head revision if tag/date not found. See +@ref{Common options}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{rev} +Select revisions based on @var{rev}. See @ref{Common options}. + +@item -s +Short patch - one liner per file. See @ref{rdiff options}. + +@item -t +Top two diffs - last change made to the file. See +@ref{diff options}. + +@item -u +Unidiff output format. See @ref{rdiff options}. + +@item -V @var{vers} +Use RCS Version @var{vers} for keyword expansion (obsolete). See +@ref{rdiff options}. +@end table + +@item release [@var{options}] @var{directory} +Indicate that a directory is no longer in use. See +@ref{release}. + +@table @code +@item -d +Delete the given directory. See @ref{release options}. +@end table + +@item remove [@var{options}] [@var{files}@dots{}] +Remove an entry from the repository. See @ref{Removing files}. + +@table @code +@item -f +Delete the file before removing it. See @ref{Removing files}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@item rtag [@var{options}] @var{tag} @var{modules}@dots{} +Add a symbolic tag to a module. +See @ref{Revisions} and @ref{Branching and merging}. + +@table @code +@item -a +Clear tag from removed files that would not otherwise +be tagged. See @ref{Tagging add/remove}. + +@item -b +Create a branch named @var{tag}. See @ref{Branching and merging}. + +@item -D @var{date} +Tag revisions as of @var{date}. See @ref{Tagging by date/tag}. + +@item -d +Delete @var{tag}. See @ref{Modifying tags}. + +@item -F +Move @var{tag} if it already exists. See @ref{Modifying tags}. + +@item -f +Force a head revision match if tag/date not found. +See @ref{Tagging by date/tag}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -n +No execution of tag program. See @ref{Common options}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{rev} +Tag existing tag @var{rev}. See @ref{Tagging by date/tag}. +@end table + +@item status [@var{options}] @var{files}@dots{} +Display status information in a working directory. See +@ref{File status}. + +@table @code +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -v +Include tag information for file. See @ref{Tags}. +@end table + +@item tag [@var{options}] @var{tag} [@var{files}@dots{}] +Add a symbolic tag to checked out version of files. +See @ref{Revisions} and @ref{Branching and merging}. + +@table @code +@item -b +Create a branch named @var{tag}. See @ref{Branching and merging}. + +@item -c +Check that working files are unmodified. See +@ref{Tagging the working directory}. + +@item -D @var{date} +Tag revisions as of @var{date}. See @ref{Tagging by date/tag}. + +@item -d +Delete @var{tag}. See @ref{Modifying tags}. + +@item -F +Move @var{tag} if it already exists. See @ref{Modifying tags}. + +@item -f +Force a head revision match if tag/date not found. +See @ref{Tagging by date/tag}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{rev} +Tag existing tag @var{rev}. See @ref{Tagging by date/tag}. +@end table + +@item unedit [@var{options}] [@var{files}@dots{}] +Undo an edit command. See @ref{Editing files}. + +@table @code +@item -a @var{actions} +Specify actions for temporary watch, where +@var{actions} is @code{edit}, @code{unedit}, +@code{commit}, @code{all}, or @code{none}. See +@ref{Editing files}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@item update [@var{options}] [@var{files}@dots{}] +Bring work tree in sync with repository. See +@ref{update}. + +@table @code +@item -A +Reset any sticky tags/date/options. See @ref{Sticky +tags} and @ref{Keyword substitution}. + +@item -C +Overwrite locally modified files with clean copies from +the repository (the modified file is saved in +@file{.#@var{file}.@var{revision}}, however). + +@item -D @var{date} +Check out revisions as of @var{date} (is sticky). See +@ref{Common options}. + +@item -d +Create directories. See @ref{update options}. + +@item -f +Use head revision if tag/date not found. See +@ref{Common options}. + +@item -I @var{ign} +More files to ignore (! to reset). See +@ref{import options}. + +@c Probably want to use rev1/rev2 style like for diff +@c -r. Here and in on-line help. +@item -j @var{rev} +Merge in changes. See @ref{update options}. + +@item -k @var{kflag} +Use @var{kflag} keyword expansion. See +@ref{Substitution modes}. + +@item -l +Local; run only in current working directory. @xref{Recursive behavior}. + +@item -P +Prune empty directories. See @ref{Moving directories}. + +@item -p +Check out files to standard output (avoids +stickiness). See @ref{update options}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. + +@item -r @var{tag} +Checkout revision @var{tag} (is sticky). See @ref{Common options}. + +@item -W @var{spec} +More wrappers. See @ref{import options}. +@end table + +@item watch [on|off|add|remove] [@var{options}] [@var{files}@dots{}] + +on/off: turn on/off read-only checkouts of files. See +@ref{Setting a watch}. + +add/remove: add or remove notification on actions. See +@ref{Getting Notified}. + +@table @code +@item -a @var{actions} +Specify actions for temporary watch, where +@var{actions} is @code{edit}, @code{unedit}, +@code{commit}, @code{all}, or @code{none}. See +@ref{Editing files}. + +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@item watchers [@var{options}] [@var{files}@dots{}] +See who is watching a file. See @ref{Watch information}. + +@table @code +@item -l +Local; run only in current working directory. See @ref{Recursive behavior}. + +@item -R +Operate recursively (default). @xref{Recursive +behavior}. +@end table + +@end table + +@c --------------------------------------------------------------------- +@node Administrative files +@appendix Reference manual for Administrative files +@cindex Administrative files (reference) +@cindex Files, reference manual +@cindex Reference manual (files) +@cindex CVSROOT (file) + +@c FIXME? Somewhere there needs to be a more "how-to" +@c guide to writing these. I think the triggers +@c (commitinfo, loginfo, taginfo, &c) are perhaps a +@c different case than files like modules. One +@c particular issue that people sometimes are +@c (unnecessarily?) worried about is performance, and +@c the impact of writing in perl or sh or ____. +Inside the repository, in the directory +@file{$CVSROOT/CVSROOT}, there are a number of +supportive files for @sc{cvs}. You can use @sc{cvs} in a limited +fashion without any of them, but if they are set up +properly they can help make life easier. For a +discussion of how to edit them, see @ref{Intro +administrative files}. + +The most important of these files is the @file{modules} +file, which defines the modules inside the repository. + +@menu +* modules:: Defining modules +* Wrappers:: Specify binary-ness based on file name +* commit files:: The commit support files +* commitinfo:: Pre-commit checking +* verifymsg:: How are log messages evaluated? +* editinfo:: Specifying how log messages are created + (obsolete) +* loginfo:: Where should log messages be sent? +* rcsinfo:: Templates for the log messages +* cvsignore:: Ignoring files via cvsignore +* checkoutlist:: Adding your own administrative files +* history file:: History information +* Variables:: Various variables are expanded +* config:: Miscellaneous CVS configuration +@end menu + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node modules +@appendixsec The modules file +@cindex Modules (admin file) +@cindex Defining modules (reference manual) + +The @file{modules} file records your definitions of +names for collections of source code. @sc{cvs} will +use these definitions if you use @sc{cvs} to update the +modules file (use normal commands like @code{add}, +@code{commit}, etc). + +The @file{modules} file may contain blank lines and +comments (lines beginning with @samp{#}) as well as +module definitions. Long lines can be continued on the +next line by specifying a backslash (@samp{\}) as the +last character on the line. + +There are three basic types of modules: alias modules, +regular modules, and ampersand modules. The difference +between them is the way that they map files in the +repository to files in the working directory. In all +of the following examples, the top-level repository +contains a directory called @file{first-dir}, which +contains two files, @file{file1} and @file{file2}, and a +directory @file{sdir}. @file{first-dir/sdir} contains +a file @file{sfile}. + +@c FIXME: should test all the examples in this section. + +@menu +* Alias modules:: The simplest kind of module +* Regular modules:: +* Ampersand modules:: +* Excluding directories:: Excluding directories from a module +* Module options:: Regular and ampersand modules can take options +* Module program options:: How the modules ``program options'' programs + are run. +@end menu + +@node Alias modules +@appendixsubsec Alias modules +@cindex Alias modules +@cindex -a, in modules file + +Alias modules are the simplest kind of module: + +@table @code +@item @var{mname} -a @var{aliases}@dots{} +This represents the simplest way of defining a module +@var{mname}. The @samp{-a} flags the definition as a +simple alias: @sc{cvs} will treat any use of @var{mname} (as +a command argument) as if the list of names +@var{aliases} had been specified instead. +@var{aliases} may contain either other module names or +paths. When you use paths in aliases, @code{checkout} +creates all intermediate directories in the working +directory, just as if the path had been specified +explicitly in the @sc{cvs} arguments. +@end table + +For example, if the modules file contains: + +@example +amodule -a first-dir +@end example + +@noindent +then the following two commands are equivalent: + +@example +$ cvs co amodule +$ cvs co first-dir +@end example + +@noindent +and they each would provide output such as: + +@example +cvs checkout: Updating first-dir +U first-dir/file1 +U first-dir/file2 +cvs checkout: Updating first-dir/sdir +U first-dir/sdir/sfile +@end example + +@node Regular modules +@appendixsubsec Regular modules +@cindex Regular modules + +@table @code +@item @var{mname} [ options ] @var{dir} [ @var{files}@dots{} ] +In the simplest case, this form of module definition +reduces to @samp{@var{mname} @var{dir}}. This defines +all the files in directory @var{dir} as module mname. +@var{dir} is a relative path (from @code{$CVSROOT}) to a +directory of source in the source repository. In this +case, on checkout, a single directory called +@var{mname} is created as a working directory; no +intermediate directory levels are used by default, even +if @var{dir} was a path involving several directory +levels. +@end table + +For example, if a module is defined by: + +@example +regmodule first-dir +@end example + +@noindent +then regmodule will contain the files from first-dir: + +@example +$ cvs co regmodule +cvs checkout: Updating regmodule +U regmodule/file1 +U regmodule/file2 +cvs checkout: Updating regmodule/sdir +U regmodule/sdir/sfile +$ +@end example + +By explicitly specifying files in the module definition +after @var{dir}, you can select particular files from +directory @var{dir}. Here is +an example: + +@example +regfiles first-dir/sdir sfile +@end example + +@noindent +With this definition, getting the regfiles module +will create a single working directory +@file{regfiles} containing the file listed, which +comes from a directory deeper +in the @sc{cvs} source repository: + +@example +$ cvs co regfiles +U regfiles/sfile +$ +@end example + +@node Ampersand modules +@appendixsubsec Ampersand modules +@cindex Ampersand modules +@cindex &, in modules file + +A module definition can refer to other modules by +including @samp{&@var{module}} in its definition. +@example +@var{mname} [ options ] @var{&module}@dots{} +@end example + +Then getting the module creates a subdirectory for each such +module, in the directory containing the module. For +example, if modules contains + +@example +ampermod &first-dir +@end example + +then a checkout will create an @code{ampermod} directory +which contains a directory called @code{first-dir}, +which in turns contains all the directories and files +which live there. For example, the command + +@example +$ cvs co ampermod +@end example + +@noindent +will create the following files: + +@example +ampermod/first-dir/file1 +ampermod/first-dir/file2 +ampermod/first-dir/sdir/sfile +@end example + +There is one quirk/bug: the messages that @sc{cvs} +prints omit the @file{ampermod}, and thus do not +correctly display the location to which it is checking +out the files: + +@example +$ cvs co ampermod +cvs checkout: Updating first-dir +U first-dir/file1 +U first-dir/file2 +cvs checkout: Updating first-dir/sdir +U first-dir/sdir/sfile +$ +@end example + +Do not rely on this buggy behavior; it may get fixed in +a future release of @sc{cvs}. + +@c FIXCVS: What happens if regular and & modules are +@c combined, as in "ampermodule first-dir &second-dir"? +@c When I tried it, it seemed to just ignore the +@c "first-dir". I think perhaps it should be an error +@c (but this needs further investigation). +@c In addition to discussing what each one does, we +@c should put in a few words about why you would use one or +@c the other in various situations. + +@node Excluding directories +@appendixsubsec Excluding directories +@cindex Excluding directories, in modules file +@cindex !, in modules file + +An alias module may exclude particular directories from +other modules by using an exclamation mark (@samp{!}) +before the name of each directory to be excluded. + +For example, if the modules file contains: + +@example +exmodule -a !first-dir/sdir first-dir +@end example + +then checking out the module @samp{exmodule} will check +out everything in @samp{first-dir} except any files in +the subdirectory @samp{first-dir/sdir}. +@c Note that the "!first-dir/sdir" sometimes must be listed +@c before "first-dir". That seems like a probable bug, in which +@c case perhaps it should be fixed (to allow either +@c order) rather than documented. See modules4 in testsuite. + +@node Module options +@appendixsubsec Module options +@cindex Options, in modules file + +Either regular modules or ampersand modules can contain +options, which supply additional information concerning +the module. + +@table @code +@cindex -d, in modules file +@item -d @var{name} +Name the working directory something other than the +module name. +@c FIXME: Needs a bunch of examples, analogous to the +@c examples for alias, regular, and ampersand modules +@c which show where the files go without -d. + +@cindex Export program +@cindex -e, in modules file +@item -e @var{prog} +Specify a program @var{prog} to run whenever files in a +module are exported. @var{prog} runs with a single +argument, the module name. +@c FIXME: Is it run on server? client? + +@cindex Checkin program +@cindex -i, in modules file +@item -i @var{prog} +Specify a program @var{prog} to run whenever files in a +module are committed. @var{prog} runs with a single +argument, the full pathname of the affected directory +in a source repository. The @file{commitinfo}, +@file{loginfo}, and @file{verifymsg} files provide other +ways to call a program on commit. +@c FIXME: Is it run on server? client? + +@cindex Checkout program +@cindex -o, in modules file +@item -o @var{prog} +Specify a program @var{prog} to run whenever files in a +module are checked out. @var{prog} runs with a single +argument, the module name. +@c FIXME: Is it run on server? client? + +@cindex Status of a module +@cindex Module status +@cindex -s, in modules file +@item -s @var{status} +Assign a status to the module. When the module file is +printed with @samp{cvs checkout -s} the modules are +sorted according to primarily module status, and +secondarily according to the module name. This option +has no other meaning. You can use this option for +several things besides status: for instance, list the +person that is responsible for this module. + +@cindex Tag program +@cindex -t, in modules file +@item -t @var{prog} +Specify a program @var{prog} to run whenever files in a +module are tagged with @code{rtag}. @var{prog} runs +with two arguments: the module name and the symbolic +tag specified to @code{rtag}. It is not run +when @code{tag} is executed. Generally you will find +that taginfo is a better solution (@pxref{user-defined logging}). +@c FIXME: Is it run on server? client? +@c Problems with -t include: +@c * It is run after the tag not before +@c * It doesn't get passed all the information that +@c taginfo does ("mov", &c). +@c * It only is run for rtag, not tag. + +@cindex Update program +@cindex -u, in modules file +@item -u @var{prog} +Specify a program @var{prog} to run whenever @samp{cvs +update} is executed from the top-level directory of the +checked-out module. @var{prog} runs with a single +argument, the full path to the source repository for +this module. +@c FIXME: Is it run on server? client? +@c One drawback of -u and -i are that CVS/Update.prog +@c and CVS/Checkin.prog only get updated on initial +@c checkout, and don't get updated if the modules file +@c changes. Also, the user can edit them, which means +@c they are no good for security-type stuff. +@end table + +You should also see @pxref{Module program options} about how the +``program options'' programs are run. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +@node Module program options +@appendixsubsec How the modules file ``program options'' programs are run +@cindex Modules file program options +@cindex -u, in modules file +@cindex -t, in modules file +@cindex -o, in modules file +@cindex -i, in modules file +@cindex -e, in modules file + +@noindent +For checkout, rtag, and export, the program is server-based, and as such the +following applies:- + +If using remote access methods (pserver, ext, etc.), +CVS will execute this program on the server from a temporary +directory. The path is searched for this program. + +If using ``local access'' (on a local or remote NFS filesystem, i.e. +repository set just to a path), +the program will be executed from the newly checked-out tree, if +found there, or alternatively searched for in the path if not. + +@noindent +The commit and update programs are locally-based, and are run as +follows:- + +The program is always run locally. One must +re-checkout the tree one is using if these options are updated in the +modules administrative file. The file CVS/Checkin.prog contains the +value of the option `-i' set in the modules file, and similarly for +the file CVS/Update.prog and `-u'. The program is always executed from +the top level of the checked-out copy on the client. Again, the program +is first searched for in the checked-out copy and then using the path. + +Note also that the commit and update programs work ONLY when using local +repository access -- the files simply aren't created when sources are +checked out from a pserver or other remote CVS. + +The programs are all run after the operation has effectively +completed. + + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node Wrappers +@appendixsec The cvswrappers file +@cindex cvswrappers (admin file) +@cindex CVSWRAPPERS, environment variable +@cindex Wrappers + +@c FIXME: need some better way of separating this out +@c by functionality. -t/-f is one feature, -m is +@c another, and -k is a third. And this discussion +@c should be better motivated (e.g. start with the +@c problems, then explain how the feature solves it). + +Wrappers refers to a @sc{cvs} feature which lets you +control certain settings based on the name of the file +which is being operated on. The settings are @samp{-k} +for binary files, and @samp{-m} for nonmergeable text +files. + +@ignore +Wrappers allow you to set a hook which transforms files on +their way in and out of @sc{cvs}. + +The file @file{cvswrappers} defines the script that will be +run on a file when its name matches a regular +expression. There are two scripts that can be run on a +file or directory. One script is executed on the file/directory +before being checked into the repository (this is denoted +with the @code{-t} flag) and the other when the file is +checked out of the repository (this is denoted with the +@code{-f} flag). The @samp{-t}/@samp{-f} feature does +not work with client/server @sc{cvs}. +@c I think maybe -t/-f works client/server if a single +@c file converts to/from a single file, as opposed to +@c the file<->directory case. Could use more +@c investigation... +@end ignore + +The @samp{-m} option +specifies the merge methodology that should be used when +a non-binary file is updated. @code{MERGE} means the usual +@sc{cvs} behavior: try to merge the files. @code{COPY} +means that @code{cvs update} will refuse to merge +files, as it also does for files specified as binary +with @samp{-kb} (but if the file is specified as +binary, there is no need to specify @samp{-m 'COPY'}). +CVS will provide the user with the +two versions of the files, and require the user using +mechanisms outside @sc{cvs}, to insert any necessary +changes. @strong{WARNING}: do not use @code{COPY} with +@sc{cvs} 1.9 or earlier--such versions of @sc{cvs} will +copy one version of your file over the other, wiping +out the previous contents. +@c Ordinarily we don't document the behavior of old +@c versions. But this one is so dangerous, I think we +@c must. I almost renamed it to -m 'NOMERGE' so we +@c could say "never use -m 'COPY'". +The @samp{-m} wrapper option only affects behavior when +merging is done on update; it does not affect how files +are stored. See @ref{Binary files}, for more on +binary files. + +The basic format of the file @file{cvswrappers} is: + +@c FIXME: @example is all wrong for this. Use @deffn or +@c something more sensible. +@example +wildcard [option value][option value]... + +where option is one of +@ignore +-f from cvs filter value: path to filter +-t to cvs filter value: path to filter +@end ignore +-m update methodology value: MERGE or COPY +-k keyword expansion value: expansion mode + +and value is a single-quote delimited value. +@end example + +@ignore +@example +*.nib -f 'unwrap %s' -t 'wrap %s %s' -m 'COPY' +*.c -t 'indent %s %s' +@end example +@c When does the filter need to be an absolute pathname +@c and when will something like the above work? I +@c suspect it relates to the PATH of the server (which +@c in turn depends on all kinds of stuff, e.g. inetd +@c for pserver). I'm not sure whether/where to discuss +@c this. +@c FIXME: What do the %s's stand for? + +@noindent +The above example of a @file{cvswrappers} file +states that all files/directories that end with a @code{.nib} +should be filtered with the @file{wrap} program before +checking the file into the repository. The file should +be filtered though the @file{unwrap} program when the +file is checked out of the repository. The +@file{cvswrappers} file also states that a @code{COPY} +methodology should be used when updating the files in +the repository (that is, no merging should be performed). + +@c What pitfalls arise when using indent this way? Is +@c it a winning thing to do? Would be nice to at least +@c hint at those issues; we want our examples to tell +@c how to solve problems, not just to say that cvs can +@c do certain things. +The last example line says that all files that end with +@code{.c} should be filtered with @file{indent} +before being checked into the repository. Unlike the previous +example, no filtering of the @code{.c} file is done when +it is checked out of the repository. +@noindent +The @code{-t} filter is called with two arguments, +the first is the name of the file/directory to filter +and the second is the pathname to where the resulting +filtered file should be placed. + +@noindent +The @code{-f} filter is called with one argument, +which is the name of the file to filter from. The end +result of this filter will be a file in the users directory +that they can work on as they normally would. + +Note that the @samp{-t}/@samp{-f} features do not +conveniently handle one portion of CVS's operation: +determining when files are modified. CVS will still +want a file (or directory) to exist, and it will use +its modification time to determine whether a file is +modified. If CVS erroneously thinks a file is +unmodified (for example, a directory is unchanged but +one of the files within it is changed), you can force +it to check in the file anyway by specifying the +@samp{-f} option to @code{cvs commit} (@pxref{commit +options}). +@c This is, of course, a serious design flaw in -t/-f. +@c Probably the whole functionality needs to be +@c redesigned (starting from requirements) to fix this. +@end ignore + +@c FIXME: We don't document -W or point to where it is +@c documented. Or .cvswrappers. +For example, the following command imports a +directory, treating files whose name ends in +@samp{.exe} as binary: + +@example +cvs import -I ! -W "*.exe -k 'b'" first-dir vendortag reltag +@end example + +@c Another good example, would be storing files +@c (e.g. binary files) compressed in the repository. +@c :::::::::::::::::: +@c cvswrappers +@c :::::::::::::::::: +@c *.t12 -m 'COPY' +@c *.t[0-9][0-9] -f 'gunzipcp %s' -t 'gzipcp %s %s' -m 'COPY' +@c +@c :::::::::::::::::: +@c gunzipcp +@c :::::::::::::::::: +@c : +@c [ -f $1 ] || exit 1 +@c zcat $1 > /tmp/.#$1.$$ +@c mv /tmp/.#$1.$$ $1 +@c +@c :::::::::::::::::: +@c gzipcp +@c :::::::::::::::::: +@c : +@c DIRNAME=`echo $1 | sed -e "s|/.*/||g"` +@c if [ ! -d $DIRNAME ] ; then +@c DIRNAME=`echo $1 | sed -e "s|.*/||g"` +@c fi +@c gzip -c $DIRNAME > $2 +@c One catch--"cvs diff" will not invoke the wrappers +@c (probably a CVS bug, although I haven't thought it out). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node commit files +@appendixsec The commit support files +@cindex Commit files + +The @samp{-i} flag in the @file{modules} file can be +used to run a certain program whenever files are +committed (@pxref{modules}). The files described in +this section provide other, more flexible, ways to run +programs whenever something is committed. + +There are three kind of programs that can be run on +commit. They are specified in files in the repository, +as described below. The following table summarizes the +file names and the purpose of the corresponding +programs. + +@table @file +@item commitinfo +The program is responsible for checking that the commit +is allowed. If it exits with a non-zero exit status +the commit will be aborted. + +@item verifymsg +The specified program is used to evaluate the log message, +and possibly verify that it contains all required +fields. This is most useful in combination with the +@file{rcsinfo} file, which can hold a log message +template (@pxref{rcsinfo}). + +@item editinfo +The specified program is used to edit the log message, +and possibly verify that it contains all required +fields. This is most useful in combination with the +@file{rcsinfo} file, which can hold a log message +template (@pxref{rcsinfo}). (obsolete) + +@item loginfo +The specified program is called when the commit is +complete. It receives the log message and some +additional information and can store the log message in +a file, or mail it to appropriate persons, or maybe +post it to a local newsgroup, or@dots{} Your +imagination is the limit! +@end table + +@menu +* syntax:: The common syntax +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node syntax +@appendixsubsec The common syntax +@cindex Info files (syntax) +@cindex Syntax of info files +@cindex Common syntax of info files + +@c FIXME: having this so totally separate from the +@c Variables node is rather bogus. + +The administrative files such as @file{commitinfo}, +@file{loginfo}, @file{rcsinfo}, @file{verifymsg}, etc., +all have a common format. The purpose of the files are +described later on. The common syntax is described +here. + +@cindex Regular expression syntax +Each line contains the following: +@itemize @bullet +@item +@c Say anything about DEFAULT and ALL? Right now we +@c leave that to the description of each file (and in fact +@c the practice is inconsistent which is really annoying). +A regular expression. This is a basic regular +expression in the syntax used by GNU emacs. +@c FIXME: What we probably should be saying is "POSIX Basic +@c Regular Expression with the following extensions (`\(' +@c `\|' '+' etc)" +@c rather than define it with reference to emacs. +@c The reference to emacs is not strictly speaking +@c true, as we don't support \=, \s, or \S. Also it isn't +@c clear we should document and/or promise to continue to +@c support all the obscure emacs extensions like \<. +@c Also need to better cite (or include) full +@c documentation for the syntax. +@c Also see comment in configure.in about what happens to the +@c syntax if we pick up a system-supplied regexp matcher. + +@item +A whitespace separator---one or more spaces and/or tabs. + +@item +A file name or command-line template. +@end itemize + +@noindent +Blank lines are ignored. Lines that start with the +character @samp{#} are treated as comments. Long lines +unfortunately can @emph{not} be broken in two parts in +any way. + +The first regular expression that matches the current +directory name in the repository is used. The rest of the line +is used as a file name or command-line as appropriate. + +@c FIXME: need an example. In particular, show what +@c the regular expression is matched against (one +@c ordinarily clueful person got confused about whether it +@c includes the filename--"directory name" above should be +@c unambiguous but there is nothing like an example to +@c confirm people's understanding of this sort of thing). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node commitinfo +@appendixsec Commitinfo +@cindex Commitinfo +@cindex Checking commits +@cindex Precommit checking + +The @file{commitinfo} file defines programs to execute +whenever @samp{cvs commit} is about to execute. These +programs are used for pre-commit checking to verify +that the modified, added and removed files are really +ready to be committed. This could be used, for +instance, to verify that the changed files conform to +to your site's standards for coding practice. + +As mentioned earlier, each line in the +@file{commitinfo} file consists of a regular expression +and a command-line template. The template can include +a program name and any number of arguments you wish to +supply to it. The full path to the current source +repository is appended to the template, followed by the +file names of any files involved in the commit (added, +removed, and modified files). + +@cindex Exit status, of commitinfo +The first line with a regular expression matching the +directory within the repository will be used. If the +command returns a non-zero exit status the commit will +be aborted. +@c FIXME: need example(s) of what "directory within the +@c repository" means. + +@cindex DEFAULT in commitinfo +If the repository name does not match any of the +regular expressions in this file, the @samp{DEFAULT} +line is used, if it is specified. + +@cindex ALL in commitinfo +All occurrences of the name @samp{ALL} appearing as a +regular expression are used in addition to the first +matching regular expression or the name @samp{DEFAULT}. + +Note: when @sc{cvs} is accessing a remote repository, +@file{commitinfo} will be run on the @emph{remote} +(i.e., server) side, not the client side (@pxref{Remote +repositories}). + +@c FIXME: should discuss using commitinfo to control +@c who has checkin access to what (e.g. Joe can check into +@c directories a, b, and c, and Mary can check into +@c directories b, c, and d--note this case cannot be +@c conveniently handled with unix groups). Of course, +@c adding a new set of features to CVS might be a more +@c natural way to fix this problem than telling people to +@c use commitinfo. +@c FIXME: Should make some reference, especially in +@c the context of controlling who has access, to the fact +@c that commitinfo can be circumvented. Perhaps +@c mention SETXID (but has it been carefully examined +@c for holes?). This fits in with the discussion of +@c general CVS security in "Password authentication +@c security" (the bit which is not pserver-specific). + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node verifymsg +@appendixsec Verifying log messages +@cindex verifymsg (admin file) +@cindex Log message, verifying + +Once you have entered a log message, you can evaluate +that message to check for specific content, such as +a bug ID. Use the @file{verifymsg} file to +specify a program that is used to verify the log message. +This program could be a simple script that checks +that the entered message contains the required fields. + +The @file{verifymsg} file is often most useful together +with the @file{rcsinfo} file, which can be used to +specify a log message template. + +Each line in the @file{verifymsg} file consists of a +regular expression and a command-line template. The +template must include a program name, and can include +any number of arguments. The full path to the current +log message template file is appended to the template. + +One thing that should be noted is that the @samp{ALL} +keyword is not supported. If more than one matching +line is found, the first one is used. This can be +useful for specifying a default verification script in a +directory, and then overriding it in a subdirectory. + +@cindex DEFAULT in verifymsg +If the repository name does not match any of the +regular expressions in this file, the @samp{DEFAULT} +line is used, if it is specified. + +@cindex Exit status, of verifymsg +If the verification script exits with a non-zero exit status, +the commit is aborted. + +Note that the verification script cannot change the log +message; it can merely accept it or reject it. +@c FIXME? Is this an annoying limitation? It would be +@c relatively easy to fix (although it would *not* be a +@c good idea for a verifymsg script to interact with the user +@c at least in the client/server case because of locks +@c and all that jazz). + +The following is a little silly example of a +@file{verifymsg} file, together with the corresponding +@file{rcsinfo} file, the log message template and an +verification script. We begin with the log message template. +We want to always record a bug-id number on the first +line of the log message. The rest of log message is +free text. The following template is found in the file +@file{/usr/cvssupport/tc.template}. + +@example +BugId: +@end example + +The script @file{/usr/cvssupport/bugid.verify} is used to +evaluate the log message. + +@example +#!/bin/sh +# +# bugid.verify filename +# +# Verify that the log message contains a valid bugid +# on the first line. +# +if head -1 < $1 | grep '^BugId:[ ]*[0-9][0-9]*$' > /dev/null; then + exit 0 +else + echo "No BugId found." + exit 1 +fi +@end example + +The @file{verifymsg} file contains this line: + +@example +^tc /usr/cvssupport/bugid.verify +@end example + +The @file{rcsinfo} file contains this line: + +@example +^tc /usr/cvssupport/tc.template +@end example + + + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node editinfo +@appendixsec Editinfo +@cindex editinfo (admin file) +@cindex Editor, specifying per module +@cindex Per-module editor +@cindex Log messages, editing + +@emph{NOTE:} The @file{editinfo} feature has been +rendered obsolete. To set a default editor for log +messages use the @code{EDITOR} environment variable +(@pxref{Environment variables}) or the @samp{-e} global +option (@pxref{Global options}). See @ref{verifymsg}, +for information on the use of the @file{verifymsg} +feature for evaluating log messages. + +If you want to make sure that all log messages look the +same way, you can use the @file{editinfo} file to +specify a program that is used to edit the log message. +This program could be a custom-made editor that always +enforces a certain style of the log message, or maybe a +simple shell script that calls an editor, and checks +that the entered message contains the required fields. + +If no matching line is found in the @file{editinfo} +file, the editor specified in the environment variable +@code{$CVSEDITOR} is used instead. If that variable is +not set, then the environment variable @code{$EDITOR} +is used instead. If that variable is not +set a default will be used. See @ref{Committing your changes}. + +The @file{editinfo} file is often most useful together +with the @file{rcsinfo} file, which can be used to +specify a log message template. + +Each line in the @file{editinfo} file consists of a +regular expression and a command-line template. The +template must include a program name, and can include +any number of arguments. The full path to the current +log message template file is appended to the template. + +One thing that should be noted is that the @samp{ALL} +keyword is not supported. If more than one matching +line is found, the first one is used. This can be +useful for specifying a default edit script in a +module, and then overriding it in a subdirectory. + +@cindex DEFAULT in editinfo +If the repository name does not match any of the +regular expressions in this file, the @samp{DEFAULT} +line is used, if it is specified. + +If the edit script exits with a non-zero exit status, +the commit is aborted. + +Note: when @sc{cvs} is accessing a remote repository, +or when the @samp{-m} or @samp{-F} options to @code{cvs +commit} are used, @file{editinfo} will not be consulted. +There is no good workaround for this; use +@file{verifymsg} instead. + +@menu +* editinfo example:: Editinfo example +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node editinfo example +@appendixsubsec Editinfo example + +The following is a little silly example of a +@file{editinfo} file, together with the corresponding +@file{rcsinfo} file, the log message template and an +editor script. We begin with the log message template. +We want to always record a bug-id number on the first +line of the log message. The rest of log message is +free text. The following template is found in the file +@file{/usr/cvssupport/tc.template}. + +@example +BugId: +@end example + +The script @file{/usr/cvssupport/bugid.edit} is used to +edit the log message. + +@example +#!/bin/sh +# +# bugid.edit filename +# +# Call $EDITOR on FILENAME, and verify that the +# resulting file contains a valid bugid on the first +# line. +if [ "x$EDITOR" = "x" ]; then EDITOR=vi; fi +if [ "x$CVSEDITOR" = "x" ]; then CVSEDITOR=$EDITOR; fi +$CVSEDITOR $1 +until head -1|grep '^BugId:[ ]*[0-9][0-9]*$' < $1 +do echo -n "No BugId found. Edit again? ([y]/n)" + read ans + case $@{ans@} in + n*) exit 1;; + esac + $CVSEDITOR $1 +done +@end example + +The @file{editinfo} file contains this line: + +@example +^tc /usr/cvssupport/bugid.edit +@end example + +The @file{rcsinfo} file contains this line: + +@example +^tc /usr/cvssupport/tc.template +@end example + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node loginfo +@appendixsec Loginfo +@cindex loginfo (admin file) +@cindex Storing log messages +@cindex Mailing log messages +@cindex Distributing log messages +@cindex Log messages + +@c "cvs commit" is not quite right. What we +@c mean is "when the repository gets changed" which +@c also includes "cvs import" and "cvs add" on a directory. +The @file{loginfo} file is used to control where +@samp{cvs commit} log information is sent. The first +entry on a line is a regular expression which is tested +against the directory that the change is being made to, +relative to the @code{$CVSROOT}. If a match is found, then +the remainder of the line is a filter program that +should expect log information on its standard input. + +If the repository name does not match any of the +regular expressions in this file, the @samp{DEFAULT} +line is used, if it is specified. + +All occurrences of the name @samp{ALL} appearing as a +regular expression are used in addition to the first +matching regular expression or @samp{DEFAULT}. + +The first matching regular expression is used. + +@xref{commit files}, for a description of the syntax of +the @file{loginfo} file. + +The user may specify a format string as +part of the filter. The string is composed of a +@samp{%} followed by a space, or followed by a single +format character, or followed by a set of format +characters surrounded by @samp{@{} and @samp{@}} as +separators. The format characters are: + +@table @t +@item s +file name +@item V +old version number (pre-checkin) +@item v +new version number (post-checkin) +@end table + +All other characters that appear in a format string +expand to an empty field (commas separating fields are +still provided). + +For example, some valid format strings are @samp{%}, +@samp{%s}, @samp{%@{s@}}, and @samp{%@{sVv@}}. + +The output will be a string of tokens separated by +spaces. For backwards compatibility, the first +token will be the repository subdirectory. The rest of the +tokens will be comma-delimited lists of the information +requested in the format string. For example, if +@samp{/u/src/master/yoyodyne/tc} is the repository, @samp{%@{sVv@}} +is the format string, and three files (@t{ChangeLog}, +@t{Makefile}, @t{foo.c}) were modified, the output +might be: + +@example +yoyodyne/tc ChangeLog,1.1,1.2 Makefile,1.3,1.4 foo.c,1.12,1.13 +@end example + +As another example, @samp{%@{@}} means that only the +name of the repository will be generated. + +Note: when @sc{cvs} is accessing a remote repository, +@file{loginfo} will be run on the @emph{remote} +(i.e., server) side, not the client side (@pxref{Remote +repositories}). + +@menu +* loginfo example:: Loginfo example +* Keeping a checked out copy:: Updating a tree on every checkin +@end menu + +@c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +@node loginfo example +@appendixsubsec Loginfo example + +The following @file{loginfo} file, together with the +tiny shell-script below, appends all log messages +to the file @file{$CVSROOT/CVSROOT/commitlog}, +and any commits to the administrative files (inside +the @file{CVSROOT} directory) are also logged in +@file{/usr/adm/cvsroot-log}. +Commits to the @file{prog1} directory are mailed to @t{ceder}. + +@c FIXME: is it a CVS feature or bug that only the +@c first matching line is used? It is documented +@c above, but is it useful? For example, if we wanted +@c to run both "cvs-log" and "Mail" for the CVSROOT +@c directory, it is kind of awkward if +@c only the first matching line is used. +@example +ALL /usr/local/bin/cvs-log $CVSROOT/CVSROOT/commitlog $USER +^CVSROOT /usr/local/bin/cvs-log /usr/adm/cvsroot-log +^prog1 Mail -s %s ceder +@end example + +The shell-script @file{/usr/local/bin/cvs-log} looks +like this: + +@example +#!/bin/sh +(echo "------------------------------------------------------"; + echo -n $2" "; + date; + echo; + cat) >> $1 +@end example + +@node Keeping a checked out copy +@appendixsubsec Keeping a checked out copy + +@c What other index entries? It seems like +@c people might want to use a lot of different +@c words for this functionality. +@cindex Keeping a checked out copy +@cindex Checked out copy, keeping +@cindex Web pages, maintaining with CVS + +It is often useful to maintain a directory tree which +contains files which correspond to the latest version +in the repository. For example, other developers might +want to refer to the latest sources without having to +check them out, or you might be maintaining a web site +with @sc{cvs} and want every checkin to cause the files +used by the web server to be updated. +@c Can we offer more details on the web example? Or +@c point the user at how to figure it out? This text +@c strikes me as sufficient for someone who already has +@c some idea of what we mean but not enough for the naive +@c user/sysadmin to understand it and set it up. + +The way to do this is by having loginfo invoke +@code{cvs update}. Doing so in the naive way will +cause a problem with locks, so the @code{cvs update} +must be run in the background. +@c Should we try to describe the problem with locks? +@c It seems like a digression for someone who just +@c wants to know how to make it work. +@c Another choice which might work for a single file +@c is to use "cvs -n update -p" which doesn't take +@c out locks (I think) but I don't see many advantages +@c of that and we might as well document something which +@c works for multiple files. +Here is an example for unix (this should all be on one line): + +@example +^cyclic-pages (date; cat; (sleep 2; cd /u/www/local-docs; + cvs -q update -d) &) >> $CVSROOT/CVSROOT/updatelog 2>&1 +@end example + +This will cause checkins to repository directories +starting with @code{cyclic-pages} to update the checked +out tree in @file{/u/www/local-docs}. +@c More info on some of the details? The "sleep 2" is +@c so if we are lucky the lock will be gone by the time +@c we start and we can wait 2 seconds instead of 30. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node rcsinfo +@appendixsec Rcsinfo +@cindex rcsinfo (admin file) +@cindex Form for log message +@cindex Log message template +@cindex Template for log message + +The @file{rcsinfo} file can be used to specify a form to +edit when filling out the commit log. The +@file{rcsinfo} file has a syntax similar to the +@file{verifymsg}, @file{commitinfo} and @file{loginfo} +files. @xref{syntax}. Unlike the other files the second +part is @emph{not} a command-line template. Instead, +the part after the regular expression should be a full pathname to +a file containing the log message template. + +If the repository name does not match any of the +regular expressions in this file, the @samp{DEFAULT} +line is used, if it is specified. + +All occurrences of the name @samp{ALL} appearing as a +regular expression are used in addition to the first +matching regular expression or @samp{DEFAULT}. + +@c FIXME: should be offering advice, somewhere around +@c here, about where to put the template file. The +@c verifymsg example uses /usr/cvssupport but doesn't +@c say anything about what that directory is for or +@c whether it is hardwired into CVS or who creates +@c it or anything. In particular we should say +@c how to version control the template file. A +@c probably better answer than the /usr/cvssupport +@c stuff is to use checkoutlist (with xref to the +@c checkoutlist doc). +@c Also I am starting to see a connection between +@c this and the Keeping a checked out copy node. +@c Probably want to say something about that. +The log message template will be used as a default log +message. If you specify a log message with @samp{cvs +commit -m @var{message}} or @samp{cvs commit -f +@var{file}} that log message will override the +template. + +@xref{verifymsg}, for an example @file{rcsinfo} +file. + +When @sc{cvs} is accessing a remote repository, +the contents of @file{rcsinfo} at the time a directory +is first checked out will specify a template which does +not then change. If you edit @file{rcsinfo} or its +templates, you may need to check out a new working +directory. +@c Would be nice to fix CVS so this isn't needed. For +@c example, a mechanism analogous to CVS/Entries, where +@c the client keeps track of what version of the template +@c it has. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node cvsignore +@appendixsec Ignoring files via cvsignore +@cindex cvsignore (admin file), global +@cindex Global cvsignore +@cindex Ignoring files +@c -- This chapter should maybe be moved to the +@c tutorial part of the manual? + +There are certain file names that frequently occur +inside your working copy, but that you don't want to +put under @sc{cvs} control. Examples are all the object +files that you get while you compile your sources. +Normally, when you run @samp{cvs update}, it prints a +line for each file it encounters that it doesn't know +about (@pxref{update output}). + +@sc{cvs} has a list of files (or sh(1) file name patterns) +that it should ignore while running @code{update}, +@code{import} and @code{release}. +@c -- Are those the only three commands affected? +This list is constructed in the following way. + +@itemize @bullet +@item +The list is initialized to include certain file name +patterns: names associated with @sc{cvs} +administration, or with other common source control +systems; common names for patch files, object files, +archive files, and editor backup files; and other names +that are usually artifacts of assorted utilities. +Currently, the default list of ignored file name +patterns is: + +@cindex Ignored files +@cindex Automatically ignored files +@example + RCS SCCS CVS CVS.adm + RCSLOG cvslog.* + tags TAGS + .make.state .nse_depinfo + *~ #* .#* ,* _$* *$ + *.old *.bak *.BAK *.orig *.rej .del-* + *.a *.olb *.o *.obj *.so *.exe + *.Z *.elc *.ln + core +@end example + +@item +The per-repository list in +@file{$CVSROOT/CVSROOT/cvsignore} is appended to +the list, if that file exists. + +@item +The per-user list in @file{.cvsignore} in your home +directory is appended to the list, if it exists. + +@item +Any entries in the environment variable +@code{$CVSIGNORE} is appended to the list. + +@item +Any @samp{-I} options given to @sc{cvs} is appended. + +@item +As @sc{cvs} traverses through your directories, the contents +of any @file{.cvsignore} will be appended to the list. +The patterns found in @file{.cvsignore} are only valid +for the directory that contains them, not for +any sub-directories. +@end itemize + +In any of the 5 places listed above, a single +exclamation mark (@samp{!}) clears the ignore list. +This can be used if you want to store any file which +normally is ignored by @sc{cvs}. + +Specifying @samp{-I !} to @code{cvs import} will import +everything, which is generally what you want to do if +you are importing files from a pristine distribution or +any other source which is known to not contain any +extraneous files. However, looking at the rules above +you will see there is a fly in the ointment; if the +distribution contains any @file{.cvsignore} files, then +the patterns from those files will be processed even if +@samp{-I !} is specified. The only workaround is to +remove the @file{.cvsignore} files in order to do the +import. Because this is awkward, in the future +@samp{-I !} might be modified to override +@file{.cvsignore} files in each directory. + +Note that the syntax of the ignore files consists of a +series of lines, each of which contains a space +separated list of filenames. This offers no clean way +to specify filenames which contain spaces, but you can +use a workaround like @file{foo?bar} to match a file +named @file{foo bar} (it also matches @file{fooxbar} +and the like). Also note that there is currently no +way to specify comments. +@c FIXCVS? I don't _like_ this syntax at all, but +@c changing it raises all the usual compatibility +@c issues and I'm also not sure what to change it to. + +@node checkoutlist +@appendixsec The checkoutlist file +@cindex checkoutlist + +It may be helpful to use @sc{cvs} to maintain your own +files in the @file{CVSROOT} directory. For example, +suppose that you have a script @file{logcommit.pl} +which you run by including the following line in the +@file{commitinfo} administrative file: + +@example +ALL $CVSROOT/CVSROOT/logcommit.pl +@end example + +To maintain @file{logcommit.pl} with @sc{cvs} you would +add the following line to the @file{checkoutlist} +administrative file: + +@example +logcommit.pl +@end example + +The format of @file{checkoutlist} is one line for each +file that you want to maintain using @sc{cvs}, giving +the name of the file. + +After setting up @file{checkoutlist} in this fashion, +the files listed there will function just like +@sc{cvs}'s built-in administrative files. For example, +when checking in one of the files you should get a +message such as: + +@example +cvs commit: Rebuilding administrative file database +@end example + +and the checked out copy in the @file{CVSROOT} +directory should be updated. + +Note that listing @file{passwd} (@pxref{Password +authentication server}) in @file{checkoutlist} is not +recommended for security reasons. + +For information about keeping a checkout out copy in a +more general context than the one provided by +@file{checkoutlist}, see @ref{Keeping a checked out +copy}. + +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@node history file +@appendixsec The history file +@cindex History file +@cindex Log information, saving + +The file @file{$CVSROOT/CVSROOT/history} is used +to log information for the @code{history} command +(@pxref{history}). This file must be created to turn +on logging. This is done automatically if the +@code{cvs init} command is used to set up the +repository (@pxref{Creating a repository}). + +The file format of the @file{history} file is +documented only in comments in the @sc{cvs} source +code, but generally programs should use the @code{cvs +history} command to access it anyway, in case the +format changes with future releases of @sc{cvs}. + +@node Variables +@appendixsec Expansions in administrative files +@cindex Internal variables +@cindex Variables + +Sometimes in writing an administrative file, you might +want the file to be able to know various things based +on environment @sc{cvs} is running in. There are +several mechanisms to do that. + +To find the home directory of the user running @sc{cvs} +(from the @code{HOME} environment variable), use +@samp{~} followed by @samp{/} or the end of the line. +Likewise for the home directory of @var{user}, use +@samp{~@var{user}}. These variables are expanded on +the server machine, and don't get any reasonable +expansion if pserver (@pxref{Password authenticated}) +is in use; therefore user variables (see below) may be +a better choice to customize behavior based on the user +running @sc{cvs}. +@c Based on these limitations, should we deprecate ~? +@c What is it good for? Are people using it? + +One may want to know about various pieces of +information internal to @sc{cvs}. A @sc{cvs} internal +variable has the syntax @code{$@{@var{variable}@}}, +where @var{variable} starts with a letter and consists +of alphanumeric characters and @samp{_}. If the +character following @var{variable} is a +non-alphanumeric character other than @samp{_}, the +@samp{@{} and @samp{@}} can be omitted. The @sc{cvs} +internal variables are: + +@table @code +@item CVSROOT +@cindex CVSROOT, internal variable +This is the value of the @sc{cvs} root in use. +@xref{Repository}, for a description of the various +ways to specify this. + +@item RCSBIN +@cindex RCSBIN, internal variable +In @sc{cvs} 1.9.18 and older, this specified the +directory where @sc{cvs} was looking for @sc{rcs} +programs. Because @sc{cvs} no longer runs @sc{rcs} +programs, specifying this internal variable is now an +error. + +@item CVSEDITOR +@cindex CVSEDITOR, internal variable +@itemx VISUAL +@cindex VISUAL, internal variable +@itemx EDITOR +@cindex EDITOR, internal variable +These all expand to the same value, which is the editor +that @sc{cvs} is using. @xref{Global options}, for how +to specify this. + +@item USER +@cindex USER, internal variable +Username of the user running @sc{cvs} (on the @sc{cvs} +server machine). +@end table + +If you want to pass a value to the administrative files +which the user who is running @sc{cvs} can specify, +use a user variable. +@cindex User variables +To expand a user variable, the +administrative file contains +@code{$@{=@var{variable}@}}. To set a user variable, +specify the global option @samp{-s} to @sc{cvs}, with +argument @code{@var{variable}=@var{value}}. It may be +particularly useful to specify this option via +@file{.cvsrc} (@pxref{~/.cvsrc}). + +For example, if you want the administrative file to +refer to a test directory you might create a user +variable @code{TESTDIR}. Then if @sc{cvs} is invoked +as + +@example +cvs -s TESTDIR=/work/local/tests +@end example + +@noindent +and the +administrative file contains @code{sh +$@{=TESTDIR@}/runtests}, then that string is expanded +to @code{sh /work/local/tests/runtests}. + +All other strings containing @samp{$} are reserved; +there is no way to quote a @samp{$} character so that +@samp{$} represents itself. + +@node config +@appendixsec The CVSROOT/config configuration file + +@cindex config, in CVSROOT +@cindex CVSROOT/config + +The administrative file @file{config} contains various +miscellaneous settings which affect the behavior of +@sc{cvs}. The syntax is slightly different from the +other administrative files. Variables are not +expanded. Lines which start with @samp{#} are +considered comments. +@c FIXME: where do we define comments for the other +@c administrative files. +Other lines consist of a keyword, @samp{=}, and a +value. Note that this syntax is very strict. +Extraneous spaces or tabs are not permitted. +@c See comments in parseinfo.c:parse_config for more +@c discussion of this strictness. + +Currently defined keywords are: + +@table @code +@cindex RCSBIN, in CVSROOT/config +@item RCSBIN=@var{bindir} +For @sc{cvs} 1.9.12 through 1.9.18, this setting told +@sc{cvs} to look for @sc{rcs} programs in the +@var{bindir} directory. Current versions of @sc{cvs} +do not run @sc{rcs} programs; for compatibility this +setting is accepted, but it does nothing. + +@cindex SystemAuth, in CVSROOT/config +@item SystemAuth=@var{value} +If @var{value} is @samp{yes}, then pserver should check +for users in the system's user database if not found in +@file{CVSROOT/passwd}. If it is @samp{no}, then all +pserver users must exist in @file{CVSROOT/passwd}. +The default is @samp{yes}. For more on pserver, see +@ref{Password authenticated}. + +@cindex PreservePermissions, in CVSROOT/config +@item PreservePermissions=@var{value} +Enable support for saving special device files, +symbolic links, file permissions and ownerships in the +repository. The default value is @samp{no}. +@xref{Special Files}, for the full implications of using +this keyword. + +@cindex TopLevelAdmin, in CVSROOT/config +@item TopLevelAdmin=@var{value} +Modify the @samp{checkout} command to create a +@samp{CVS} directory at the top level of the new +working directory, in addition to @samp{CVS} +directories created within checked-out directories. +The default value is @samp{no}. + +This option is useful if you find yourself performing +many commands at the top level of your working +directory, rather than in one of the checked out +subdirectories. The @file{CVS} directory created there +will mean you don't have to specify @code{CVSROOT} for +each command. It also provides a place for the +@file{CVS/Template} file (@pxref{Working directory +storage}). + +@cindex LockDir, in CVSROOT/config +@item LockDir=@var{directory} +Put CVS lock files in @var{directory} rather than +directly in the repository. This is useful if you want +to let users read from the repository while giving them +write access only to @var{directory}, not to the +repository. You need to create @var{directory}, but +CVS will create subdirectories of @var{directory} as it +needs them. For information on CVS locks, see +@ref{Concurrency}. + +@c Mention this in Compatibility section? +Before enabling the LockDir option, make sure that you +have tracked down and removed any copies of CVS 1.9 or +older. Such versions neither support LockDir, nor will +give an error indicating that they don't support it. +The result, if this is allowed to happen, is that some +CVS users will put the locks one place, and others will +put them another place, and therefore the repository +could become corrupted. CVS 1.10 does not support +LockDir but it will print a warning if run on a +repository with LockDir enabled. +@end table + +@c --------------------------------------------------------------------- +@node Environment variables +@appendix All environment variables which affect CVS +@cindex Environment variables +@cindex Reference manual for variables + +This is a complete list of all environment variables +that affect @sc{cvs}. + +@table @code +@cindex CVSIGNORE, environment variable +@item $CVSIGNORE +A whitespace-separated list of file name patterns that +@sc{cvs} should ignore. @xref{cvsignore}. + +@cindex CVSWRAPPERS, environment variable +@item $CVSWRAPPERS +A whitespace-separated list of file name patterns that +@sc{cvs} should treat as wrappers. @xref{Wrappers}. + +@cindex CVSREAD, environment variable +@cindex Read-only files, and CVSREAD +@item $CVSREAD +If this is set, @code{checkout} and @code{update} will +try hard to make the files in your working directory +read-only. When this is not set, the default behavior +is to permit modification of your working files. + +@item $CVSUMASK +Controls permissions of files in the repository. See +@ref{File permissions}. + +@item $CVSROOT +Should contain the full pathname to the root of the @sc{cvs} +source repository (where the @sc{rcs} files are +kept). This information must be available to @sc{cvs} for +most commands to execute; if @code{$CVSROOT} is not set, +or if you wish to override it for one invocation, you +can supply it on the command line: @samp{cvs -d cvsroot +cvs_command@dots{}} Once you have checked out a working +directory, @sc{cvs} stores the appropriate root (in +the file @file{CVS/Root}), so normally you only need to +worry about this when initially checking out a working +directory. + +@item $EDITOR +@itemx $CVSEDITOR +@itemx $VISUAL +Specifies the program to use for recording log messages +during commit. @code{$CVSEDITOR} overrides +@code{$EDITOR}. See @ref{Committing your changes}. + +@cindex PATH, environment variable +@item $PATH +If @code{$RCSBIN} is not set, and no path is compiled +into @sc{cvs}, it will use @code{$PATH} to try to find all +programs it uses. + +@cindex HOME, environment variable +@item $HOME +@cindex HOMEPATH, environment variable +@item $HOMEPATH +@cindex HOMEDRIVE, environment variable +@item $HOMEDRIVE +Used to locate the directory where the @file{.cvsrc} +file, and other such files, are searched. On Unix, CVS +just checks for @code{HOME}. On Windows NT, the system will +set @code{HOMEDRIVE}, for example to @samp{d:} and @code{HOMEPATH}, +for example to @file{\joe}. On Windows 95, you'll +probably need to set @code{HOMEDRIVE} and @code{HOMEPATH} yourself. +@c We are being vague about whether HOME works on +@c Windows; see long comment in windows-NT/filesubr.c. + +@cindex CVS_RSH, environment variable +@item $CVS_RSH +Specifies the external program which CVS connects with, +when @code{:ext:} access method is specified. +@pxref{Connecting via rsh}. + +@item $CVS_SERVER +Used in client-server mode when accessing a remote +repository using @sc{rsh}. It specifies the name of +the program to start on the server side when accessing +a remote repository using @sc{rsh}. The default value +is @code{cvs}. @pxref{Connecting via rsh} + +@item $CVS_PASSFILE +Used in client-server mode when accessing the @code{cvs +login server}. Default value is @file{$HOME/.cvspass}. +@pxref{Password authentication client} + +@item $CVS_CLIENT_PORT +Used in client-server mode when accessing the server +via Kerberos. +@pxref{Kerberos authenticated} + +@cindex CVS_RCMD_PORT, environment variable +@item $CVS_RCMD_PORT +Used in client-server mode. If set, specifies the port +number to be used when accessing the @sc{rcmd} demon on +the server side. (Currently not used for Unix clients). + +@cindex CVS_CLIENT_LOG, environment variable +@item $CVS_CLIENT_LOG +Used for debugging only in client-server +mode. If set, everything sent to the server is logged +into @file{@code{$CVS_CLIENT_LOG}.in} and everything +sent from the server is logged into +@file{@code{$CVS_CLIENT_LOG}.out}. + +@cindex CVS_SERVER_SLEEP, environment variable +@item $CVS_SERVER_SLEEP +Used only for debugging the server side in +client-server mode. If set, delays the start of the +server child process the specified amount of +seconds so that you can attach to it with a debugger. + +@cindex CVS_IGNORE_REMOTE_ROOT, environment variable +@item $CVS_IGNORE_REMOTE_ROOT +For @sc{cvs} 1.10 and older, setting this variable +prevents @sc{cvs} from overwriting the @file{CVS/Root} +file when the @samp{-d} global option is specified. +Later versions of @sc{cvs} do not rewrite +@file{CVS/Root}, so @code{CVS_IGNORE_REMOTE_ROOT} has no +effect. + +@cindex COMSPEC, environment variable +@item $COMSPEC +Used under OS/2 only. It specifies the name of the +command interpreter and defaults to @sc{cmd.exe}. + +@cindex TMPDIR, environment variable +@item $TMPDIR +@cindex TMP, environment variable +@itemx $TMP +@cindex TEMP, environment variable +@itemx $TEMP +@cindex Temporary files, location of +@c This is quite nuts. We don't talk about tempnam +@c or mkstemp which we sometimes use. The discussion +@c of "Global options" is semi-incoherent. +@c I'm not even sure those are the only inaccuracies. +@c Furthermore, the conventions are +@c pretty crazy and they should be simplified. +Directory in which temporary files are located. +The @sc{cvs} server uses +@code{TMPDIR}. @xref{Global options}, for a +description of how to specify this. +Some parts of @sc{cvs} will always use @file{/tmp} (via +the @code{tmpnam} function provided by the system). + +On Windows NT, @code{TMP} is used (via the @code{_tempnam} +function provided by the system). + +The @code{patch} program which is used by the @sc{cvs} +client uses @code{TMPDIR}, and if it is not set, uses +@file{/tmp} (at least with GNU patch 2.1). Note that +if your server and client are both running @sc{cvs} +1.9.10 or later, @sc{cvs} will not invoke an external +@code{patch} program. +@end table + +@node Compatibility +@appendix Compatibility between CVS Versions + +@cindex CVS, versions of +@cindex Versions, of CVS +@cindex Compatibility, between CVS versions +@c We don't mention versions older than CVS 1.3 +@c on the theory that it would clutter it up for the vast +@c majority of people, who don't have anything that old. +@c +The repository format is compatible going back to +@sc{cvs} 1.3. But see @ref{Watches Compatibility}, if +you have copies of @sc{cvs} 1.6 or older and you want +to use the optional developer communication features. +@c If you "cvs rm" and commit using 1.3, then you'll +@c want to run "rcs -sdead <file,v>" on each of the +@c files in the Attic if you then want 1.5 and +@c later to recognize those files as dead (I think the +@c symptom if this is not done is that files reappear +@c in joins). (Wait: the above will work but really to +@c be strictly correct we should suggest checking +@c in a new revision rather than just changing the +@c state of the head revision, shouldn't we?). +@c The old convert.sh script was for this, but it never +@c did get updated to reflect use of the RCS "dead" +@c state. +@c Note: this is tricky to document without confusing +@c people--need to carefully say what CVS version we +@c are talking about and keep in mind the distinction +@c between a +@c repository created with 1.3 and on which one now +@c uses 1.5+, and a repository on which one wants to +@c use both versions side by side (e.g. during a +@c transition period). +@c Wait, can't CVS just detect the case in which a file +@c is in the Attic but the head revision is not dead? +@c Not sure whether this should produce a warning or +@c something, and probably needs further thought, but +@c it would appear that the situation can be detected. +@c +@c We might want to separate out the 1.3 compatibility +@c section (for repository & working directory) from the +@c rest--that might help avoid confusing people who +@c are upgrading (for example) from 1.6 to 1.8. +@c +@c A minor incompatibility is if a current version of CVS +@c puts "Nfoo" into CVS/Tag, then CVS 1.9 or older will +@c see this as if there is no tag. Seems to me this is +@c too obscure to mention. + +The working directory format is compatible going back +to @sc{cvs} 1.5. It did change between @sc{cvs} 1.3 +and @sc{cvs} 1.5. If you run @sc{cvs} 1.5 or newer on +a working directory checked out with @sc{cvs} 1.3, +@sc{cvs} will convert it, but to go back to @sc{cvs} +1.3 you need to check out a new working directory with +@sc{cvs} 1.3. + +The remote protocol is interoperable going back to @sc{cvs} 1.5, but no +further (1.5 was the first official release with the remote protocol, +but some older versions might still be floating around). In many +cases you need to upgrade both the client and the server to take +advantage of new features and bugfixes, however. + +@c Perhaps should be saying something here about the +@c "D" lines in Entries (written by CVS 1.9; 1.8 and +@c older don't use them). These are supposed to be +@c compatible in both directions, but I'm not sure +@c they quite are 100%. One common gripe is if you +@c "rm -r" a directory and 1.9 gets confused, as it +@c still sees it in Entries. That one is fixed in +@c (say) 1.9.6. Someone else reported problems with +@c starting with a directory which was checked out with +@c an old version, and then using a new version, and +@c some "D" lines appeared, but not for every +@c directory, causing some directories to be skipped. +@c They weren't sure how to reproduce this, though. + +@c --------------------------------------------------------------------- +@node Troubleshooting +@appendix Troubleshooting + +If you are having trouble with @sc{cvs}, this appendix +may help. If there is a particular error message which +you are seeing, then you can look up the message +alphabetically. If not, you can look through the +section on other problems to see if your problem is +mentioned there. + +@menu +* Error messages:: Partial list of CVS errors +* Connection:: Trouble making a connection to a CVS server +* Other problems:: Problems not readily listed by error message +@end menu + +@ignore +@c - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@c @node Bad administrative files +@appendixsec Bad administrative files + +@c -- Give hints on how to fix them +@end ignore + +@node Error messages +@appendixsec Partial list of error messages + +Here is a partial list of error messages that you may +see from @sc{cvs}. It is not a complete list---@sc{cvs} +is capable of printing many, many error messages, often +with parts of them supplied by the operating system, +but the intention is to list the common and/or +potentially confusing error messages. + +The messages are alphabetical, but introductory text +such as @samp{cvs update: } is not considered in +ordering them. + +In some cases the list includes messages printed by old +versions of @sc{cvs} (partly because users may not be +sure which version of @sc{cvs} they are using at any +particular moment). +@c If we want to start retiring messages, perhaps we +@c should pick a cutoff version (for example, no more +@c messages which are specific to versions before 1.9) +@c and then move the old messages to an "old messages" +@c node rather than deleting them completely. + +@table @code +@c FIXME: What is the correct way to format a multiline +@c error message here? Maybe @table is the wrong +@c choice? Texinfo gurus? +@item cvs @var{command}: authorization failed: server @var{host} rejected access +This is a generic response when trying to connect to a +pserver server which chooses not to provide a +specific reason for denying authorization. Check that +the username and password specified are correct and +that the @code{CVSROOT} specified is allowed by @samp{--allow-root} +in @file{inetd.conf}. See @ref{Password authenticated}. + +@item @var{file}:@var{line}: Assertion '@var{text}' failed +The exact format of this message may vary depending on +your system. It indicates a bug in @sc{cvs}, which can +be handled as described in @ref{BUGS}. + +@item cvs @var{command}: conflict: removed @var{file} was modified by second party +This message indicates that you removed a file, and +someone else modified it. To resolve the conflict, +first run @samp{cvs add @var{file}}. If desired, look +at the other party's modification to decide whether you +still want to remove it. If you don't want to remove +it, stop here. If you do want to remove it, proceed +with @samp{cvs remove @var{file}} and commit your +removal. +@c Tests conflicts2-142b* in sanity.sh test for this. + +@item cannot change permissions on temporary directory +@example +Operation not permitted +@end example +This message has been happening in a non-reproducible, +occasional way when we run the client/server testsuite, +both on Red Hat Linux 3.0.3 and 4.1. We haven't been +able to figure out what causes it, nor is it known +whether it is specific to linux (or even to this +particular machine!). If the problem does occur on +other unices, @samp{Operation not permitted} would be +likely to read @samp{Not owner} or whatever the system +in question uses for the unix @code{EPERM} error. If +you have any information to add, please let us know as +described in @ref{BUGS}. If you experience this error +while using @sc{cvs}, retrying the operation which +produced it should work fine. +@c This has been seen in a variety of tests, including +@c multibranch-2, multibranch-5, and basic1-24-rm-rm, +@c so it doesn't seem particularly specific to any one +@c test. + +@item cvs [server aborted]: Cannot check out files into the repository itself +The obvious cause for this message (especially for +non-client/server @sc{cvs}) is that the @sc{cvs} root +is, for example, @file{/usr/local/cvsroot} and you try +to check out files when you are in a subdirectory, such +as @file{/usr/local/cvsroot/test}. However, there is a +more subtle cause, which is that the temporary +directory on the server is set to a subdirectory of the +root (which is also not allowed). If this is the +problem, set the temporary directory to somewhere else, +for example @file{/var/tmp}; see @code{TMPDIR} in +@ref{Environment variables}, for how to set the +temporary directory. + +@c For one example see basica-1a10 in the testsuite +@c For another example, "cvs co ." on NT; see comment +@c at windows-NT/filesubr.c (expand_wild). +@c For another example, "cvs co foo/bar" where foo exists. +@item cannot open CVS/Entries for reading: No such file or directory +This generally indicates a @sc{cvs} internal error, and +can be handled as with other @sc{cvs} bugs +(@pxref{BUGS}). Usually there is a workaround---the +exact nature of which would depend on the situation but +which hopefully could be figured out. + +@c This is more obscure than it might sound; it only +@c happens if you run "cvs init" from a directory which +@c contains a CVS/Root file at the start. +@item cvs [init aborted]: cannot open CVS/Root: No such file or directory +This message is harmless. Provided it is not +accompanied by other errors, the operation has +completed successfully. This message should not occur +with current versions of @sc{cvs}, but it is documented +here for the benefit of @sc{cvs} 1.9 and older. + +@item cvs [checkout aborted]: cannot rename file @var{file} to CVS/,,@var{file}: Invalid argument +This message has been reported as intermittently +happening with CVS 1.9 on Solaris 2.5. The cause is +unknown; if you know more about what causes it, let us +know as described in @ref{BUGS}. + +@item cvs [@var{command} aborted]: cannot start server via rcmd +This, unfortunately, is a rather nonspecific error +message which @sc{cvs} 1.9 will print if you are +running the @sc{cvs} client and it is having trouble +connecting to the server. Current versions of @sc{cvs} +should print a much more specific error message. If +you get this message when you didn't mean to run the +client at all, you probably forgot to specify +@code{:local:}, as described in @ref{Repository}. + +@item ci: @var{file},v: bad diff output line: Binary files - and /tmp/T2a22651 differ +CVS 1.9 and older will print this message +when trying to check in a binary file if +@sc{rcs} is not correctly installed. Re-read the +instructions that came with your @sc{rcs} distribution +and the @sc{install} file in the @sc{cvs} +distribution. Alternately, upgrade to a current +version of @sc{cvs}, which checks in files itself +rather than via @sc{rcs}. + +@item cvs checkout: could not check out @var{file} +With CVS 1.9, this can mean that the @code{co} program +(part of @sc{rcs}) returned a failure. It should be +preceded by another error message, however it has been +observed without another error message and the cause is +not well-understood. With the current version of CVS, +which does not run @code{co}, if this message occurs +without another error message, it is definitely a CVS +bug (@pxref{BUGS}). +@c My current suspicion is that the RCS in the rcs (not +@c cvs/winnt/rcs57nt.zip) directory on the _Practical_ +@c CD is bad (remains to be confirmed). +@c There is also a report of something which looks +@c very similar on SGI, Irix 5.2, so I dunno. + +@item cvs [login aborted]: could not find out home directory +This means that you need to set the environment +variables that CVS uses to locate your home directory. +See the discussion of @code{HOME}, @code{HOMEDRIVE}, and @code{HOMEPATH} in +@ref{Environment variables}. + +@item cvs update: could not merge revision @var{rev} of @var{file}: No such file or directory +CVS 1.9 and older will print this message if there was +a problem finding the @code{rcsmerge} program. Make +sure that it is in your @code{PATH}, or upgrade to a +current version of CVS, which does not require +an external @code{rcsmerge} program. + +@item cvs [update aborted]: could not patch @var{file}: No such file or directory +This means that there was a problem finding the +@code{patch} program. Make sure that it is in your +@code{PATH}. Note that despite appearances the message +is @emph{not} referring to whether it can find @var{file}. +If both the client and the server are running a current +version of @sc{cvs}, then there is no need for an +external patch program and you should not see this +message. But if either client or server is running +@sc{cvs} 1.9, then you need @code{patch}. + +@item cvs update: could not patch @var{file}; will refetch +This means that for whatever reason the client was +unable to apply a patch that the server sent. The +message is nothing to be concerned about, because +inability to apply the patch only slows things down and +has no effect on what @sc{cvs} does. +@c xref to update output. Or File status? +@c Or some place else that +@c explains this whole "patch"/P/Needs Patch thing? + +@item dying gasps from @var{server} unexpected +There is a known bug in the server for @sc{cvs} 1.9.18 +and older which can cause this. For me, this was +reproducible if I used the @samp{-t} global option. It +was fixed by Andy Piper's 14 Nov 1997 change to +src/filesubr.c, if anyone is curious. +If you see the message, +you probably can just retry the operation which failed, +or if you have discovered information concerning its +cause, please let us know as described in @ref{BUGS}. + +@item end of file from server (consult above messages if any) +The most common cause for this message is if you are +using an external @code{rsh} program and it exited with +an error. In this case the @code{rsh} program should +have printed a message, which will appear before the +above message. For more information on setting up a +@sc{cvs} client and server, see @ref{Remote repositories}. + +@item cvs [update aborted]: EOF in key in RCS file @var{file},v +@itemx cvs [checkout aborted]: EOF while looking for end of string in RCS file @var{file},v +This means that there is a syntax error in the given +@sc{rcs} file. Note that this might be true even if @sc{rcs} can +read the file OK; @sc{cvs} does more error checking of +errors in the RCS file. That is why you may see this +message when upgrading from @sc{cvs} 1.9 to @sc{cvs} +1.10. The likely cause for the original corruption is +hardware, the operating system, or the like. Of +course, if you find a case in which @sc{cvs} seems to +corrupting the file, by all means report it, +(@pxref{BUGS}). +There are quite a few variations of this error message, +depending on exactly where in the @sc{rcs} file @sc{cvs} +finds the syntax error. + +@cindex mkmodules +@item cvs commit: Executing 'mkmodules' +This means that your repository is set up for a version +of @sc{cvs} prior to @sc{cvs} 1.8. When using @sc{cvs} +1.8 or later, the above message will be preceded by + +@example +cvs commit: Rebuilding administrative file database +@end example + +If you see both messages, the database is being rebuilt +twice, which is unnecessary but harmless. If you wish +to avoid the duplication, and you have no versions of +@sc{cvs} 1.7 or earlier in use, remove @code{-i mkmodules} +every place it appears in your @code{modules} +file. For more information on the @code{modules} file, +see @ref{modules}. + +@c This message comes from "co", and I believe is +@c possible only with older versions of CVS which call +@c co. The problem with being able to create the bogus +@c RCS file still exists, though (and I think maybe +@c there is a different symptom(s) now). +@c FIXME: Would be nice to have a more exact wording +@c for this message. +@item missing author +Typically this can happen if you created an RCS file +with your username set to empty. CVS will, bogusly, +create an illegal RCS file with no value for the author +field. The solution is to make sure your username is +set to a non-empty value and re-create the RCS file. +@c "make sure your username is set" is complicated in +@c and of itself, as there are the environment +@c variables the system login name, &c, and it depends +@c on the version of CVS. + +@item cvs [checkout aborted]: no such tag @var{tag} +This message means that @sc{cvs} isn't familiar with +the tag @var{tag}. Usually this means that you have +mistyped a tag name; however there are (relatively +obscure) cases in which @sc{cvs} will require you to +@c Search sanity.sh for "no such tag" to see some of +@c the relatively obscure cases. +try a few other @sc{cvs} commands involving that tag, +before you find one which will cause @sc{cvs} to update +the @file{val-tags} file; see discussion of val-tags in +@ref{File permissions}. You only need to worry about +this once for a given tag; when a tag is listed in +@file{val-tags}, it stays there. Note that using +@samp{-f} to not require tag matches does not override +this check; see @ref{Common options}. + +@item *PANIC* administration files missing +This typically means that there is a directory named +CVS but it does not contain the administrative files +which CVS puts in a CVS directory. If the problem is +that you created a CVS directory via some mechanism +other than CVS, then the answer is simple, use a name +other than CVS. If not, it indicates a CVS bug +(@pxref{BUGS}). + +@item rcs error: Unknown option: -x,v/ +This message will be followed by a usage message for +@sc{rcs}. It means that you have an old version of +@sc{rcs} (probably supplied with your operating +system). CVS only works with @sc{rcs} version 5 and +later. +@c For more information on installing @sc{cvs}, see +@c (FIXME: where? it depends on whether you are +@c getting binaries or sources or what). +@c The message can also say "ci error" or something +@c instead of "rcs error", I suspect. + +@item cvs [server aborted]: received broken pipe signal +This message seems to be caused by a hard-to-track-down +bug in @sc{cvs} or the systems it runs on (we don't +know---we haven't tracked it down yet!). It seems to +happen only after a @sc{cvs} command has completed, and +you should be able to just ignore the message. +However, if you have discovered information concerning its +cause, please let us know as described in @ref{BUGS}. + +@item Too many arguments! +This message is typically printed by the @file{log.pl} +script which is in the @file{contrib} directory in the +@sc{cvs} source distribution. In some versions of +@sc{cvs}, @file{log.pl} has been part of the default +@sc{cvs} installation. The @file{log.pl} script gets +called from the @file{loginfo} administrative file. +Check that the arguments passed in @file{loginfo} match +what your version of @file{log.pl} expects. In +particular, the @file{log.pl} from @sc{cvs} 1.3 and +older expects the logfile as an argument whereas the +@file{log.pl} from @sc{cvs} 1.5 and newer expects the +logfile to be specified with a @samp{-f} option. Of +course, if you don't need @file{log.pl} you can just +comment it out of @file{loginfo}. + +@item cvs [update aborted]: unexpected EOF reading @var{file},v +See @samp{EOF in key in RCS file}. + +@item cvs [login aborted]: unrecognized auth response from @var{server} +This message typically means that the server is not set +up properly. For example, if @file{inetd.conf} points +to a nonexistent cvs executable. To debug it further, +find the log file which inetd writes +(@file{/var/log/messages} or whatever inetd uses on +your system). For details, see @ref{Connection}, and +@ref{Password authentication server}. + +@item cvs commit: Up-to-date check failed for `@var{file}' +This means that someone else has committed a change to +that file since the last time that you did a @code{cvs +update}. So before proceeding with your @code{cvs +commit} you need to @code{cvs update}. CVS will merge +the changes that you made and the changes that the +other person made. If it does not detect any conflicts +it will report @samp{M cacErrCodes.h} and you are ready +to @code{cvs commit}. If it detects conflicts it will +print a message saying so, will report @samp{C cacErrCodes.h}, +and you need to manually resolve the +conflict. For more details on this process see +@ref{Conflicts example}. + +@item Usage: diff3 [-exEX3 [-i | -m] [-L label1 -L label3]] file1 file2 file3 +@example +Only one of [exEX3] allowed +@end example +This indicates a problem with the installation of +@code{diff3} and @code{rcsmerge}. Specifically +@code{rcsmerge} was compiled to look for GNU diff3, but +it is finding unix diff3 instead. The exact text of +the message will vary depending on the system. The +simplest solution is to upgrade to a current version of +@sc{cvs}, which does not rely on external +@code{rcsmerge} or @code{diff3} programs. + +@item warning: unrecognized response `@var{text}' from cvs server +If @var{text} contains a valid response (such as +@samp{ok}) followed by an extra carriage return +character (on many systems this will cause the second +part of the message to overwrite the first part), then +it probably means that you are using the @samp{:ext:} +access method with a version of rsh, such as most +non-unix rsh versions, which does not by default +provide a transparent data stream. In such cases you +probably want to try @samp{:server:} instead of +@samp{:ext:}. If @var{text} is something else, this +may signify a problem with your CVS server. +Double-check your installation against the instructions +for setting up the CVS server. +@c FIXCVS: should be printing CR as \r or \015 or some +@c such, probably. + +@item cvs commit: [@var{time}] waiting for @var{user}'s lock in @var{directory} +This is a normal message, not an error. See +@ref{Concurrency}, for more details. + +@item cvs commit: warning: editor session failed +@cindex Exit status, of editor +This means that the editor which @sc{cvs} is using exits with a nonzero +exit status. Some versions of vi will do this even when there was not +a problem editing the file. If so, point the +@code{CVSEDITOR} environment variable to a small script +such as: + +@example +#!/bin/sh +vi $* +exit 0 +@end example + +@c "warning: foo was lost" and "no longer pertinent" (both normal). +@c Would be nice to write these up--they are +@c potentially confusing for the new user. +@end table + +@node Connection +@appendixsec Trouble making a connection to a CVS server + +This section concerns what to do if you are having +trouble making a connection to a @sc{cvs} server. If +you are running the @sc{cvs} command line client +running on Windows, first upgrade the client to +@sc{cvs} 1.9.12 or later. The error reporting in +earlier versions provided much less information about +what the problem was. If the client is non-Windows, +@sc{cvs} 1.9 should be fine. + +If the error messages are not sufficient to track down +the problem, the next steps depend largely on which +access method you are using. + +@table @code +@cindex :ext:, troubleshooting +@item :ext: +Try running the rsh program from the command line. For +example: "rsh servername cvs -v" should print @sc{cvs} +version information. If this doesn't work, you need to +fix it before you can worry about @sc{cvs} problems. + +@cindex :server:, troubleshooting +@item :server: +You don't need a command line rsh program to use this +access method, but if you have an rsh program around, +it may be useful as a debugging tool. Follow the +directions given for :ext:. + +@cindex :pserver:, troubleshooting +@item :pserver: +One good debugging tool is to "telnet servername +2401". After connecting, send any text (for example +"foo" followed by return). If @sc{cvs} is working +correctly, it will respond with + +@example +cvs [pserver aborted]: bad auth protocol start: foo +@end example + +If this fails to work, then make sure inetd is working +right. Change the invocation in @file{inetd.conf} to run the +echo program instead of cvs. For example: + +@example +2401 stream tcp nowait root /bin/echo echo hello +@end example + +After making that change and instructing inetd to +re-read its configuration file, "telnet servername +2401" should show you the text hello and then the +server should close the connection. If this doesn't +work, you need to fix it before you can worry about +@sc{cvs} problems. + +On AIX systems, the system will often have its own +program trying to use port 2401. This is AIX's problem +in the sense that port 2401 is registered for use with +@sc{cvs}. I hear that there is an AIX patch available +to address this problem. + +Another good debugging tool is the @samp{-d} +(debugging) option to inetd. Consult your system +documentation for more information. + +If you seem to be connecting but get errors like: + +@example +cvs server: cannot open /root/.cvsignore: Permission denied +cvs [server aborted]: can't chdir(/root): Permission denied +@end example + +then either you haven't specified @samp{-f} in @file{inetd.conf} +or your system is setting the @code{HOME} environment variable +for programs being run by inetd. In the latter case, you can either +have inetd run a shell script that unsets @code{HOME} and then runs +@sc{cvs}, or you can use @code{env} to run @sc{cvs} with a pristine +environment. +@end table + +@node Other problems +@appendixsec Other common problems + +Here is a list of problems which do not fit into the +above categories. They are in no particular order. + +@itemize @bullet +@item +On Windows, if there is a 30 second or so delay when +you run a @sc{cvs} command, it may mean that you have +your home directory set to @file{C:/}, for example (see +@code{HOMEDRIVE} and @code{HOMEPATH} in +@ref{Environment variables}). CVS expects the home +directory to not end in a slash, for example @file{C:} +or @file{C:\cvs}. +@c FIXCVS: CVS should at least detect this and print an +@c error, presumably. + +@item +If you are running @sc{cvs} 1.9.18 or older, and +@code{cvs update} finds a conflict and tries to +merge, as described in @ref{Conflicts example}, but +doesn't tell you there were conflicts, then you may +have an old version of @sc{rcs}. The easiest solution +probably is to upgrade to a current version of +@sc{cvs}, which does not rely on external @sc{rcs} +programs. +@end itemize + +@c --------------------------------------------------------------------- +@node Credits +@appendix Credits + +@cindex Contributors (manual) +@cindex Credits (manual) +Roland Pesch, then of Cygnus Support <@t{roland@@wrs.com}> +wrote the manual pages which were distributed with +@sc{cvs} 1.3. Much of their text was copied into this +manual. He also read an early draft +of this manual and contributed many ideas and +corrections. + +The mailing-list @code{info-cvs} is sometimes +informative. I have included information from postings +made by the following persons: +David G. Grubbs <@t{dgg@@think.com}>. + +Some text has been extracted from the man pages for +@sc{rcs}. + +The @sc{cvs} @sc{faq} by David G. Grubbs has provided +useful material. The @sc{faq} is no longer maintained, +however, and this manual is about the closest thing there +is to a successor (with respect to documenting how to +use @sc{cvs}, at least). + +In addition, the following persons have helped by +telling me about mistakes I've made: + +@display +Roxanne Brunskill <@t{rbrunski@@datap.ca}>, +Kathy Dyer <@t{dyer@@phoenix.ocf.llnl.gov}>, +Karl Pingle <@t{pingle@@acuson.com}>, +Thomas A Peterson <@t{tap@@src.honeywell.com}>, +Inge Wallin <@t{ingwa@@signum.se}>, +Dirk Koschuetzki <@t{koschuet@@fmi.uni-passau.de}> +and Michael Brown <@t{brown@@wi.extrel.com}>. +@end display + +The list of contributors here is not comprehensive; for a more +complete list of who has contributed to this manual see +the file @file{doc/ChangeLog} in the @sc{cvs} source +distribution. + +@c --------------------------------------------------------------------- +@node BUGS +@appendix Dealing with bugs in CVS or this manual + +@cindex Bugs in this manual or CVS +Neither @sc{cvs} nor this manual is perfect, and they +probably never will be. If you are having trouble +using @sc{cvs}, or think you have found a bug, there +are a number of things you can do about it. Note that +if the manual is unclear, that can be considered a bug +in the manual, so these problems are often worth doing +something about as well as problems with @sc{cvs} itself. + +@cindex Reporting bugs +@cindex Bugs, reporting +@cindex Errors, reporting +@itemize @bullet +@item +If you want someone to help you and fix bugs that you +report, there are companies which will do that for a +fee. Two such companies are: + +@cindex Signum Support +@cindex Cyclic Software +@cindex Support, getting CVS support +@example +Signum Support AB +Box 2044 +S-580 02 Linkoping +Sweden +Email: info@@signum.se +Phone: +46 (0)13 - 21 46 00 +Fax: +46 (0)13 - 21 47 00 +http://www.signum.se/ + +Cyclic Software +United States of America +http://www.cyclic.com/ +info@@cyclic.com +@end example + +@item +If you got @sc{cvs} through a distributor, such as an +operating system vendor or a vendor of freeware +@sc{cd-rom}s, you may wish to see whether the +distributor provides support. Often, they will provide +no support or minimal support, but this may vary from +distributor to distributor. + +@item +If you have the skills and time to do so, you may wish +to fix the bug yourself. If you wish to submit your +fix for inclusion in future releases of @sc{cvs}, see +the file @sc{hacking} in the @sc{cvs} source +distribution. It contains much more information on the +process of submitting fixes. + +@item +There may be resources on the net which can help. Two +good places to start are: + +@example +http://www.cyclic.com +http://www.loria.fr/~molli/cvs-index.html +@end example + +If you are so inspired, increasing the information +available on the net is likely to be appreciated. For +example, before the standard @sc{cvs} distribution +worked on Windows 95, there was a web page with some +explanation and patches for running @sc{cvs} on Windows +95, and various people helped out by mentioning this +page on mailing lists or newsgroups when the subject +came up. + +@item +It is also possible to report bugs to @code{bug-cvs}. +Note that someone may or may not want to do anything +with your bug report---if you need a solution consider +one of the options mentioned above. People probably do +want to hear about bugs which are particularly severe +in consequences and/or easy to fix, however. You can +also increase your odds by being as clear as possible +about the exact nature of the bug and any other +relevant information. The way to report bugs is to +send email to @code{bug-cvs@@gnu.org}. Note +that submissions to @code{bug-cvs} may be distributed +under the terms of the @sc{gnu} Public License, so if +you don't like this, don't submit them. There is +usually no justification for sending mail directly to +one of the @sc{cvs} maintainers rather than to +@code{bug-cvs}; those maintainers who want to hear +about such bug reports read @code{bug-cvs}. Also note +that sending a bug report to other mailing lists or +newsgroups is @emph{not} a substitute for sending it to +@code{bug-cvs}. It is fine to discuss @sc{cvs} bugs on +whatever forum you prefer, but there are not +necessarily any maintainers reading bug reports sent +anywhere except @code{bug-cvs}. +@end itemize + +@cindex Known bugs in this manual or CVS +People often ask if there is a list of known bugs or +whether a particular bug is a known one. The file +@sc{bugs} in the @sc{cvs} source distribution is one +list of known bugs, but it doesn't necessarily try to +be comprehensive. Perhaps there will never be a +comprehensive, detailed list of known bugs. + +@c --------------------------------------------------------------------- +@node Index +@unnumbered Index +@cindex Index + +@printindex cp + +@summarycontents + +@contents + +@bye + +Local Variables: +fill-column: 55 +End: diff --git a/gnu/dist/cvs/doc/cvsclient.texi b/gnu/dist/cvs/doc/cvsclient.texi new file mode 100644 index 00000000000..de3595dc872 --- /dev/null +++ b/gnu/dist/cvs/doc/cvsclient.texi @@ -0,0 +1,2073 @@ +\input texinfo @c -*- texinfo -*- + +@setfilename cvsclient.info +@include CVSvn.texi + +@dircategory Programming +@direntry +* cvsclient: (cvsclient). The CVS client/server protocol. +@end direntry + +@node Top +@top CVS Client/Server + +This document describes the client/server protocol used by CVS. It does +not describe how to use or administer client/server CVS; see the regular +CVS manual for that. This is version @value{CVSVN} of the protocol +specification---@xref{Introduction}, for more on what this version number +means. + +@menu +* Introduction:: What is CVS and what is the client/server protocol for? +* Goals:: Basic design decisions, requirements, scope, etc. +* Connection and Authentication:: Various ways to connect to the server +* Password scrambling:: Scrambling used by pserver +* Protocol:: Complete description of the protocol +* Protocol Notes:: Possible enhancements, limitations, etc. of the protocol +@end menu + +@node Introduction +@chapter Introduction + +CVS is a version control system (with some additional configuration +management functionality). It maintains a central @dfn{repository} +which stores files (often source code), including past versions, +information about who modified them and when, and so on. People who +wish to look at or modify those files, known as @dfn{developers}, use +CVS to @dfn{check out} a @dfn{working directory} from the repository, to +@dfn{check in} new versions of files to the repository, and other +operations such as viewing the modification history of a file. If +developers are connected to the repository by a network, particularly a +slow or flaky one, the most efficient way to use the network is with the +CVS-specific protocol described in this document. + +Developers, using the machine on which they store their working +directory, run the CVS @dfn{client} program. To perform operations +which cannot be done locally, it connects to the CVS @dfn{server} +program, which maintains the repository. For more information on how +to connect see @ref{Connection and Authentication}. + +This document describes the CVS protocol. Unfortunately, it does not +yet completely document one aspect of the protocol---the detailed +operation of each CVS command and option---and one must look at the CVS +user documentation, @file{cvs.texinfo}, for that information. The +protocol is non-proprietary (anyone who wants to is encouraged to +implement it) and an implementation, known as CVS, is available under +the GNU Public License. The CVS distribution, containing this +implementation, @file{cvs.texinfo}, and a copy (possibly more or less up +to date than what you are reading now) of this document, +@file{cvsclient.texi}, can be found at the usual GNU FTP sites, with a +filename such as @file{cvs-@var{version}.tar.gz}. + +This is version @value{CVSVN} of the protocol specification. This +version number is intended only to aid in distinguishing different +versions of this specification. Although the specification is currently +maintained in conjunction with the CVS implementation, and carries the +same version number, it also intends to document what is involved with +interoperating with other implementations (such as other versions of +CVS); see @ref{Requirements}. This version number should not be used +by clients or servers to determine what variant of the protocol to +speak; they should instead use the @code{valid-requests} and +@code{Valid-responses} mechanism (@pxref{Protocol}), which is more +flexible. + +@node Goals +@chapter Goals + +@itemize @bullet +@item +Do not assume any access to the repository other than via this protocol. +It does not depend on NFS, rdist, etc. + +@item +Providing a reliable transport is outside this protocol. The protocol +expects a reliable transport that is transparent (that is, there is no +translation of characters, including characters such as such as +linefeeds or carriage returns), and can transmit all 256 octets (for +example for proper handling of binary files, compression, and +encryption). The encoding of characters specified by the protocol (the +names of requests and so on) is the invariant ISO 646 character set (a +subset of most popular character sets including ASCII and others). For +more details on running the protocol over the TCP reliable transport, +see @ref{Connection and Authentication}. + +@item +Security and authentication are handled outside this protocol (but see +below about @samp{cvs kserver} and @samp{cvs pserver}). + +@item +The protocol makes it possible for updates to be atomic with respect to +checkins; that is if someone commits changes to several files in one cvs +command, then an update by someone else would either get all the +changes, or none of them. The current @sc{cvs} server can't do this, +but that isn't the protocol's fault. + +@item +The protocol is, with a few exceptions, transaction-based. That is, the +client sends all its requests (without waiting for server responses), +and then waits for the server to send back all responses (without +waiting for further client requests). This has the advantage of +minimizing network turnarounds and the disadvantage of sometimes +transferring more data than would be necessary if there were a richer +interaction. Another, more subtle, advantage is that there is no need +for the protocol to provide locking for features such as making checkins +atomic with respect to updates. Any such locking can be handled +entirely by the server. A good server implementation (such as the +current @sc{cvs} server) will make sure that it does not have any such +locks in place whenever it is waiting for communication with the client; +this prevents one client on a slow or flaky network from interfering +with the work of others. + +@item +It is a general design goal to provide only one way to do a given +operation (where possible). For example, implementations have no choice +about whether to terminate lines with linefeeds or some other +character(s), and request and response names are case-sensitive. This +is to enhance interoperability. If a protocol allows more than one way +to do something, it is all too easy for some implementations to support +only some of them (perhaps accidentally). +@c I vaguely remember reading, probably in an RFC, about the problems +@c that were caused when some people decided that SMTP should accept +@c other line termination (in the message ("DATA")?) than CRLF. However, I +@c can't seem to track down the reference. +@end itemize + +@node Connection and Authentication +@chapter How to Connect to and Authenticate Oneself to the CVS server + +Connection and authentication occurs before the CVS protocol itself is +started. There are several ways to connect. + +@table @asis +@item server +If the client has a way to execute commands on the server, and provide +input to the commands and output from them, then it can connect that +way. This could be the usual rsh (port 514) protocol, Kerberos rsh, +SSH, or any similar mechanism. The client may allow the user to specify +the name of the server program; the default is @code{cvs}. It is +invoked with one argument, @code{server}. Once it invokes the server, +the client proceeds to start the cvs protocol. + +@item kserver +The kerberized server listens on a port (in the current implementation, +by having inetd call "cvs kserver") which defaults to 1999. The client +connects, sends the usual kerberos authentication information, and then +starts the cvs protocol. Note: port 1999 is officially registered for +another use, and in any event one cannot register more than one port for +CVS, so GSS-API (see below) is recommended instead of kserver as a way +to support kerberos. + +@item pserver +The name @dfn{pserver} is somewhat confusing. It refers to both a +generic framework which allows the CVS protocol to support several +authentication mechanisms, and a name for a specific mechanism which +transfers a username and a cleartext password. Servers need not support +all mechanisms, and in fact servers will typically want to support only +those mechanisms which meet the relevant security needs. + +The pserver server listens on a port (in the current +implementation, by having inetd call "cvs pserver") which defaults to +2401 (this port is officially registered). The client +connects, and sends the following: + +@itemize @bullet +@item +the string @samp{BEGIN AUTH REQUEST}, a linefeed, +@item +the cvs root, a linefeed, +@item +the username, a linefeed, +@item +the password trivially encoded (see @ref{Password scrambling}), a +linefeed, +@item +the string @samp{END AUTH REQUEST}, and a linefeed. +@end itemize + +The client must send the +identical string for cvs root both here and later in the +@code{Root} request of the cvs +protocol itself. Servers are encouraged to enforce this restriction. +The possible server responses (each of which is followed by a linefeed) +are the following. Note that although there is a small similarity +between this authentication protocol and the cvs protocol, they are +separate. + +@table @code +@item I LOVE YOU +The authentication is successful. The client proceeds with the cvs +protocol itself. + +@item I HATE YOU +The authentication fails. After sending this response, the server may +close the connection. It is up to the server to decide whether to give +this response, which is generic, or a more specific response using +@samp{E} and/or @samp{error}. + +@item E @var{text} +Provide a message for the user. After this reponse, the authentication +protocol continues with another response. Typically the server will +provide a series of @samp{E} responses followed by @samp{error}. +Compatibility note: @sc{cvs} 1.9.10 and older clients will print +@code{unrecognized auth response} and @var{text}, and then exit, upon +receiving this response. + +@item error @var{code} @var{text} +The authentication fails. After sending this response, the server may +close the connection. The @var{code} is a code describing why it +failed, intended for computer consumption. The only code currently +defined is @samp{0} which is nonspecific, but clients must silently +treat any unrecognized codes as nonspecific. +The @var{text} should be supplied to the +user. Compatibility note: @sc{cvs} 1.9.10 and older clients will print +@code{unrecognized auth response} and @var{text}, and then exit, upon +receiving this response. +Note that @var{text} for this response, or the @var{text} in an @code{E} +response, is not designed for machine parsing. More vigorous use of +@var{code}, or future extensions, will be needed to prove a cleaner +machine-parseable indication of what the error was. +@end table + +@c If you are thinking of putting samp or code around BEGIN AUTH REQUEST +@c and friends, watch for overfull hboxes. +If the client wishes to merely authenticate without starting the cvs +protocol, the procedure is the same, except BEGIN AUTH REQUEST is +replaced with BEGIN VERIFICATION REQUEST, END AUTH REQUEST +is replaced with END VERIFICATION REQUEST, and upon receipt of +I LOVE YOU the connection is closed rather than continuing. + +Another mechanism is GSSAPI authentication. GSSAPI is a +generic interface to security services such as kerberos. GSSAPI is +specified in RFC2078 (GSSAPI version 2) and RFC1508 (GSSAPI version 1); +we are not aware of differences between the two which affect the +protocol in incompatible ways, so we make no attempt to specify one +version or the other. +The procedure here is to start with @samp{BEGIN +GSSAPI REQUEST}. GSSAPI authentication information is then exchanged +between the client and the server. Each packet of information consists +of a two byte big endian length, followed by that many bytes of data. +After the GSSAPI authentication is complete, the server continues with +the responses described above (@samp{I LOVE YOU}, etc.). + +@item future possibilities +There are a nearly unlimited number of ways to connect and authenticate. +One might want to allow access based on IP address (similar to the usual +rsh protocol but with different/no restrictions on ports < 1024), to +adopt mechanisms such as Pluggable Authentication Modules (PAM), to +allow users to run their own servers under their own usernames without +root access, or any number of other possibilities. The way to add +future mechanisms, for the most part, should be to continue to use port +2401, but to use different strings in place of @samp{BEGIN AUTH +REQUEST}. +@end table + +@node Password scrambling +@chapter Password scrambling algorithm + +The pserver authentication protocol, as described in @ref{Connection and +Authentication}, trivially encodes the passwords. This is only to +prevent inadvertent compromise; it provides no protection against even a +relatively unsophisticated attacker. For comparison, HTTP Basic +Authentication (as described in RFC2068) uses BASE64 for a similar +purpose. CVS uses its own algorithm, described here. + +The scrambled password starts with @samp{A}, which serves to identify +the scrambling algorithm in use. After that follows a single octet for +each character in the password, according to a fixed encoding. The +values are shown here, with the encoded values in decimal. Control +characters, space, and characters outside the invariant ISO 646 +character set are not shown; such characters are not recommended for use +in passwords. There is a long discussion of character set issues in +@ref{Protocol Notes}. + +@example + 0 111 P 125 p 58 +! 120 1 52 A 57 Q 55 a 121 q 113 +" 53 2 75 B 83 R 54 b 117 r 32 + 3 119 C 43 S 66 c 104 s 90 + 4 49 D 46 T 124 d 101 t 44 +% 109 5 34 E 102 U 126 e 100 u 98 +& 72 6 82 F 40 V 59 f 69 v 60 +' 108 7 81 G 89 W 47 g 73 w 51 +( 70 8 95 H 38 X 92 h 99 x 33 +) 64 9 65 I 103 Y 71 i 63 y 97 +* 76 : 112 J 45 Z 115 j 94 z 62 ++ 67 ; 86 K 50 k 93 +, 116 < 118 L 42 l 39 +- 74 = 110 M 123 m 37 +. 68 > 122 N 91 n 61 +/ 87 ? 105 O 35 _ 56 o 48 +@end example + +@node Protocol +@chapter The CVS client/server protocol + +In the following, @samp{\n} refers to a linefeed and @samp{\t} refers to +a horizontal tab; @dfn{requests} are what the client sends and +@dfn{responses} are what the server sends. In general, the connection is +governed by the client---the server does not send responses without +first receiving requests to do so; see @ref{Response intro} for more +details of this convention. + +It is typical, early in the connection, for the client to transmit a +@code{Valid-responses} request, containing all the responses it +supports, followed by a @code{valid-requests} request, which elicits +from the server a @code{Valid-requests} response containing all the +requests it understands. In this way, the client and server each find +out what the other supports before exchanging large amounts of data +(such as file contents). + +@c Hmm, having 3 sections in this menu makes a certain amount of sense +@c but that structure gets lost in the printed manual (not sure about +@c HTML). Perhaps there is a better way. +@menu + +General protocol conventions: + +* Entries Lines:: Transmitting RCS data +* File Modes:: Read, write, execute, and possibly more... +* Filenames:: Conventions regarding filenames +* File transmissions:: How file contents are transmitted +* Strings:: Strings in various requests and responses +* Dates:: Times and dates + +The protocol itself: + +* Request intro:: General conventions relating to requests +* Requests:: List of requests +* Response intro:: General conventions relating to responses +* Response pathnames:: The "pathname" in responses +* Responses:: List of responses +* Text tags:: More details about the MT response + +An example session, and some further observations: + +* Example:: A conversation between client and server +* Requirements:: Things not to omit from an implementation +* Obsolete:: Former protocol features +@end menu + +@node Entries Lines +@section Entries Lines + +Entries lines are transmitted as: + +@example +/ @var{name} / @var{version} / @var{conflict} / @var{options} / @var{tag_or_date} +@end example + +@var{tag_or_date} is either @samp{T} @var{tag} or @samp{D} @var{date} +or empty. If it is followed by a slash, anything after the slash +shall be silently ignored. + +@var{version} can be empty, or start with @samp{0} or @samp{-}, for no +user file, new user file, or user file to be removed, respectively. + +@c FIXME: should distinguish sender and receiver behavior here; the +@c "anything else" and "does not start with" are intended for future +@c expansion, and we should specify a sender behavior. +@var{conflict}, if it starts with @samp{+}, indicates that the file had +conflicts in it. The rest of @var{conflict} is @samp{=} if the +timestamp matches the file, or anything else if it doesn't. If +@var{conflict} does not start with a @samp{+}, it is silently ignored. + +@var{options} signifies the keyword expansion options (for example +@samp{-ko}). In an @code{Entry} request, this indicates the options +that were specified with the file from the previous file updating +response (@pxref{Response intro}, for a list of file updating +responses); if the client is specifying the @samp{-k} or @samp{-A} +option to @code{update}, then it is the server which figures out what +overrides what. + +@node File Modes +@section File Modes + +A mode is any number of repetitions of + +@example +@var{mode-type} = @var{data} +@end example + +separated by @samp{,}. + +@var{mode-type} is an identifier composed of alphanumeric characters. +Currently specified: @samp{u} for user, @samp{g} for group, @samp{o} +for other (see below for discussion of whether these have their POSIX +meaning or are more loose). Unrecognized values of @var{mode-type} +are silently ignored. + +@var{data} consists of any data not containing @samp{,}, @samp{\0} or +@samp{\n}. For @samp{u}, @samp{g}, and @samp{o} mode types, data +consists of alphanumeric characters, where @samp{r} means read, @samp{w} +means write, @samp{x} means execute, and unrecognized letters are +silently ignored. + +The two most obvious ways in which the mode matters are: (1) is it +writeable? This is used by the developer communication features, and +is implemented even on OS/2 (and could be implemented on DOS), whose +notion of mode is limited to a readonly bit. (2) is it executable? +Unix CVS users need CVS to store this setting (for shell scripts and +the like). The current CVS implementation on unix does a little bit +more than just maintain these two settings, but it doesn't really have +a nice general facility to store or version control the mode, even on +unix, much less across operating systems with diverse protection +features. So all the ins and outs of what the mode means across +operating systems haven't really been worked out (e.g. should the VMS +port use ACLs to get POSIX semantics for groups?). + +@node Filenames +@section Conventions regarding transmission of file names + +In most contexts, @samp{/} is used to separate directory and file +names in filenames, and any use of other conventions (for example, +that the user might type on the command line) is converted to that +form. The only exceptions might be a few cases in which the server +provides a magic cookie which the client then repeats verbatim, but as +the server has not yet been ported beyond unix, the two rules provide +the same answer (and what to do if future server ports are operating +on a repository like e:/foo or CVS_ROOT:[FOO.BAR] has not been +carefully thought out). + +Characters outside the invariant ISO 646 character set should be avoided +in filenames. This restriction may need to be relaxed to allow for +characters such as @samp{[} and @samp{]} (see above about non-unix +servers); this has not been carefully considered (and currently +implementations probably use whatever character sets that the operating +systems they are running on allow, and/or that users specify). Of +course the most portable practice is to restrict oneself further, to the +POSIX portable filename character set as specified in POSIX.1. + +@node File transmissions +@section File transmissions + +File contents (noted below as @var{file transmission}) can be sent in +one of two forms. The simpler form is a number of bytes, followed by a +linefeed, followed by the specified number of bytes of file contents. +These are the entire contents of the specified file. Second, if both +client and server support @samp{gzip-file-contents}, a @samp{z} may +precede the length, and the `file contents' sent are actually compressed +with @samp{gzip} (RFC1952/1951) compression. The length specified is +that of the compressed version of the file. + +In neither case are the file content followed by any additional data. +The transmission of a file will end with a linefeed iff that file (or its +compressed form) ends with a linefeed. + +The encoding of file contents depends on the value for the @samp{-k} +option. If the file is binary (as specified by the @samp{-kb} option in +the appropriate place), then it is just a certain number of octets, and +the protocol contributes nothing towards determining the encoding (using +the file name is one widespread, if not universally popular, mechanism). +If the file is text (not binary), then the file is sent as a series of +lines, separated by linefeeds. If the keyword expansion is set to +something other than @samp{-ko}, then it is expected that the file +conform to the RCS expectations regarding keyword expansion---in +particular, that it is in a character set such as ASCII in which 0x24 is +a dollar sign (@samp{$}). + +@node Strings +@section Strings + +In various contexts, for example the @code{Argument} request and the +@code{M} response, one transmits what is essentially an arbitrary +string. Often this will have been supplied by the user (for example, +the @samp{-m} option to the @code{ci} request). The protocol has no +mechanism to specify the character set of such strings; it would be +fairly safe to stick to the invariant ISO 646 character set but the +existing practice is probably to just transmit whatever the user +specifies, and hope that everyone involved agrees which character set is +in use, or sticks to a common subset. + +@node Dates +@section Dates + +The protocol contains times and dates in various places. + +For the @samp{-D} option to the @code{annotate}, @code{co}, @code{diff}, +@code{export}, @code{history}, @code{rdiff}, @code{rtag}, @code{tag}, +and @code{update} requests, the server should support two formats: + +@example +26 May 1997 13:01:40 -0000 ; @r{RFC 822 as modified by RFC 1123} +5/26/1997 13:01:40 GMT ; @r{traditional} +@end example + +The former format is preferred; the latter however is sent by the CVS +command line client (versions 1.5 through at least 1.9). + +For the @samp{-d} option to the @code{log} request, servers should at +least support RFC 822/1123 format. Clients are encouraged to use this +format too (the command line CVS client, version 1.10 and older, just passed +along the date format specified by the user, however). + +The @code{Mod-time} response and @code{Checkin-time} request use RFC +822/1123 format (see the descriptions of that response and request for +details). + +For @code{Notify}, see the description of that request. + +@node Request intro +@section Request intro + +By convention, requests which begin with a capital letter do not elicit +a response from the server, while all others do -- save one. The +exception is @samp{gzip-file-contents}. Unrecognized requests will +always elicit a response from the server, even if that request begins +with a capital letter. + +The term @dfn{command} means a request which expects a response (except +@code{valid-requests}). The general model is that the client transmits +a great number of requests, but nothing happens until the very end when +the client transmits a command. Although the intention is that +transmitting several commands in one connection should be legal, +existing servers probably have some bugs with some combinations of more +than one command, and so clients may find it necessary to make several +connections in some cases. This should be thought of as a workaround +rather than a desired attribute of the protocol. + +@node Requests +@section Requests + +Here are the requests: + +@table @code +@item Root @var{pathname} \n +Response expected: no. Tell the server which @code{CVSROOT} to use. +Note that @var{pathname} is a local directory and @emph{not} a fully +qualified @code{CVSROOT} variable. @var{pathname} must +already exist; if creating a new root, use the @code{init} request, not +@code{Root}. @var{pathname} does not include the hostname of the +server, how to access the server, etc.; by the time the CVS protocol is +in use, connection, authentication, etc., are already taken care of. + +The @code{Root} request must be sent only once, and it must be sent +before any requests other than @code{Valid-responses}, +@code{valid-requests}, @code{UseUnchanged}, @code{Set} or @code{init}. + +@item Valid-responses @var{request-list} \n +Response expected: no. +Tell the server what responses the client will accept. +request-list is a space separated list of tokens. + +@item valid-requests \n +Response expected: yes. +Ask the server to send back a @code{Valid-requests} response. + +@item Directory @var{local-directory} \n +Additional data: @var{repository} \n. Response expected: no. +Tell the server what directory to use. The @var{repository} should be a +directory name from a previous server response. Note that +this both gives a default for @code{Entry} and @code{Modified} and +also for @code{ci} and the other commands; normal usage is to send +@code{Directory} for each directory in which there will be an +@code{Entry} or @code{Modified}, and then a final @code{Directory} +for the original directory, then the command. +The @var{local-directory} is relative to +the top level at which the command is occurring (i.e. the last +@code{Directory} which is sent before the command); +to indicate that top level, @samp{.} should be sent for +@var{local-directory}. + +Here is an example of where a client gets @var{repository} and +@var{local-directory}. Suppose that there is a module defined by + +@example +moddir 1dir +@end example + +That is, one can check out @code{moddir} and it will take @code{1dir} in +the repository and check it out to @code{moddir} in the working +directory. Then an initial check out could proceed like this: + +@example +C: Root /home/kingdon/zwork/cvsroot +. . . +C: Argument moddir +C: Directory . +C: /home/kingdon/zwork/cvsroot +C: co +S: Clear-sticky moddir/ +S: /home/kingdon/zwork/cvsroot/1dir/ +. . . +S: ok +@end example + +In this example the response shown is @code{Clear-sticky}, but it could +be another response instead. Note that it returns two pathnames. +The first one, @file{moddir/}, indicates the working +directory to check out into. The second one, ending in @file{1dir/}, +indicates the directory to pass back to the server in a subsequent +@code{Directory} request. For example, a subsequent @code{update} +request might look like: + +@example +C: Directory moddir +C: /home/kingdon/zwork/cvsroot/1dir +. . . +C: update +@end example + +For a given @var{local-directory}, the repository will be the same for +each of the responses, so one can use the repository from whichever +response is most convenient. Typically a client will store the +repository along with the sources for each @var{local-directory}, use +that same setting whenever operating on that @var{local-directory}, and +not update the setting as long as the @var{local-directory} exists. + +A client is free to rename a @var{local-directory} at any time (for +example, in response to an explicit user request). While it is true +that the server supplies a @var{local-directory} to the client, as noted +above, this is only the default place to put the directory. Of course, +the various @code{Directory} requests for a single command (for example, +@code{update} or @code{ci} request) should name a particular directory +with the same @var{local-directory}. + +Each @code{Directory} request specifies a brand-new +@var{local-directory} and @var{repository}; that is, +@var{local-directory} and @var{repository} are never relative to paths +specified in any previous @code{Directory} request. + +Here's a more complex example, in which we request an update of a +working directory which has been checked out from multiple places in the +repository. + +@example +C: Argument dir1 +C: Directory dir1 +C: /home/foo/repos/mod1 +. . . +C: Argument dir2 +C: Directory dir2 +C: /home/foo/repos/mod2 +. . . +C: Argument dir3 +C: Directory dir3/subdir3 +C: /home/foo/repos/mod3 +. . . +C: update +@end example + +While directories @code{dir1} and @code{dir2} will be handled in similar +fashion to the other examples given above, @code{dir3} is slightly +different from the server's standpoint. Notice that module @code{mod3} +is actually checked out into @code{dir3/subdir3}, meaning that directory +@code{dir3} is either empty or does not contain data checked out from +this repository. + +The above example will work correctly in @sc{cvs} 1.10.1 and later. The +server will descend the tree starting from all directories mentioned in +@code{Argument} requests and update those directories specifically +mentioned in @code{Directory} requests. + +Previous versions of @sc{cvs} (1.10 and earlier) do not behave the same +way. While the descent of the tree begins at all directories mentioned +in @code{Argument} requests, descent into subdirectories only occurs if +a directory has been mentioned in a @code{Directory} request. +Therefore, the above example would succeed in updating @code{dir1} and +@code{dir2}, but would skip @code{dir3} because that directory was not +specifically mentioned in a @code{Directory} request. A functional +version of the above that would run on a 1.10 or earlier server is as +follows: + +@example +C: Argument dir1 +C: Directory dir1 +C: /home/foo/repos/mod1 +. . . +C: Argument dir2 +C: Directory dir2 +C: /home/foo/repos/mod2 +. . . +C: Argument dir3 +C: Directory dir3 +C: /home/foo/repos/. +. . . +C: Directory dir3/subdir3 +C: /home/foo/repos/mod3 +. . . +C: update +@end example + +Note the extra @code{Directory dir3} request. It might be better to use +@code{Emptydir} as the repository for the @code{dir3} directory, but the +above will certainly work. + +One more peculiarity of the 1.10 and earlier protocol is the ordering of +@code{Directory} arguments. In order for a subdirectory to be +registered correctly for descent by the recursion processor, its parent +must be sent first. For example, the following would not work to update +@code{dir3/subdir3}: + +@example +. . . +C: Argument dir3 +C: Directory dir3/subdir3 +C: /home/foo/repos/mod3 +. . . +C: Directory dir3 +C: /home/foo/repos/. +. . . +C: update +@end example + +The implementation of the server in 1.10 and earlier writes the +administration files for a given directory at the time of the +@code{Directory} request. It also tries to register the directory with +its parent to mark it for recursion. In the above example, at the time +@code{dir3/subdir3} is created, the physical directory for @code{dir3} +will be created on disk, but the administration files will not have been +created. Therefore, when the server tries to register +@code{dir3/subdir3} for recursion, the operation will silently fail +because the administration files do not yet exist for @code{dir3}. + +@item Max-dotdot @var{level} \n +Response expected: no. +Tell the server that @var{level} levels of directories above the +directory which @code{Directory} requests are relative to will be +needed. For example, if the client is planning to use a +@code{Directory} request for @file{../../foo}, it must send a +@code{Max-dotdot} request with a @var{level} of at least 2. +@code{Max-dotdot} must be sent before the first @code{Directory} +request. + +@item Static-directory \n +Response expected: no. Tell the server that the directory most recently +specified with @code{Directory} should not have +additional files checked out unless explicitly requested. The client +sends this if the @code{Entries.Static} flag is set, which is controlled +by the @code{Set-static-directory} and @code{Clear-static-directory} +responses. + +@item Sticky @var{tagspec} \n +Response expected: no. Tell the server that the directory most recently +specified with @code{Directory} has a sticky tag or date @var{tagspec}. +The first character of @var{tagspec} is @samp{T} for a tag, @samp{D} +for a date, or some other character supplied by a Set-sticky response +from a previous request to the server. The remainder of @var{tagspec} +contains the actual tag or date, again as supplied by Set-sticky. + +The server should remember @code{Static-directory} and @code{Sticky} +requests for a particular directory; the client need not resend them +each time it sends a @code{Directory} request for a given directory. +However, the server is not obliged to remember them beyond the context +of a single command. + +@item Checkin-prog @var{program} \n +Response expected: no. Tell the server that the directory most recently +specified with @code{Directory} has a checkin program @var{program}. +Such a program would have been previously set with the +@code{Set-checkin-prog} response. + +@item Update-prog @var{program} \n +Response expected: no. Tell the server that the directory most recently +specified with @code{Directory} has an update program @var{program}. +Such a program would have been previously set with the +@code{Set-update-prog} response. + +@item Entry @var{entry-line} \n +Response expected: no. Tell the server what version of a file is on the +local machine. The name in @var{entry-line} is a name relative to the +directory most recently specified with @code{Directory}. If the user +is operating on only some files in a directory, @code{Entry} requests +for only those files need be included. If an @code{Entry} request is +sent without @code{Modified}, @code{Is-modified}, or @code{Unchanged}, +it means the file is +lost (does not exist in the working directory). If both @code{Entry} +and one of @code{Modified}, @code{Is-modified}, or @code{Unchanged} are +sent for the same file, @code{Entry} must be sent first. For a +given file, one can send @code{Modified}, @code{Is-modified}, or +@code{Unchanged}, but not more than one of these three. + +@item Kopt @var{option} \n +This indicates to the server which keyword expansion options to use for +the file specified by the next @code{Modified} or @code{Is-modified} +request (for example @samp{-kb} for a binary file). This is similar to +@code{Entry}, but is used for a file for which there is no entries line. +Typically this will be a file being added via an @code{add} or +@code{import} request. The client may not send both @code{Kopt} and +@code{Entry} for the same file. + +@item Checkin-time @var{time} \n +For the file specified by the next @code{Modified} request, use +@var{time} as the time of the checkin. The @var{time} is in the format +specified by RFC822 as modified by RFC1123. The client may specify any +timezone it chooses; servers will want to convert that to their own +timezone as appropriate. An example of this format is: + +@example +26 May 1997 13:01:40 -0400 +@end example + +There is no requirement that the client and server clocks be +synchronized. The client just sends its recommendation for a timestamp +(based on file timestamps or whatever), and the server should just believe +it (this means that the time might be in the future, for example). + +Note that this is not a general-purpose way to tell the server about the +timestamp of a file; that would be a separate request (if there are +servers which can maintain timestamp and time of checkin separately). + +This request should affect the @code{import} request, and may optionally +affect the @code{ci} request or other relevant requests if any. + +@item Modified @var{filename} \n +Response expected: no. Additional data: mode, \n, file transmission. +Send the server a copy of one locally modified file. @var{filename} is +a file within the most recent directory sent with @code{Directory}; it +must not contain @samp{/}. If +the user is operating on only some files in a directory, only those +files need to be included. This can also be sent without @code{Entry}, +if there is no entry for the file. + +@item Is-modified @var{filename} \n +Response expected: no. Additional data: none. Like @code{Modified}, +but used if the server only needs +to know whether the file is modified, not the contents. + +The commands which can take @code{Is-modified} instead of +@code{Modified} with no known change in behavior are: @code{admin}, +@code{diff} (if and only if two @samp{-r} or @samp{-D} options are +specified), @code{watch-on}, @code{watch-off}, @code{watch-add}, +@code{watch-remove}, @code{watchers}, @code{editors}, +@code{log}, and @code{annotate}. + +For the @code{status} command, one can send @code{Is-modified} but if +the client is using imperfect mechanisms such as timestamps to determine +whether to consider a file modified, then the behavior will be +different. That is, if one sends @code{Modified}, then the server will +actually compare the contents of the file sent and the one it derives +from to determine whether the file is genuinely modified. But if one +sends @code{Is-modified}, then the server takes the client's word for +it. A similar situation exists for @code{tag}, if the @samp{-c} option +is specified. + +Commands for which @code{Modified} is necessary are @code{co}, +@code{ci}, @code{update}, and @code{import}. + +Commands which do not need to inform the server about a working +directory, and thus should not be sending either @code{Modified} or +@code{Is-modified}: @code{rdiff}, @code{rtag}, @code{history}, +@code{init}, and @code{release}. + +Commands for which further investigation is warranted are: +@code{remove}, @code{add}, and @code{export}. Pending such +investigation, the more conservative course of action is to stick to +@code{Modified}. + +@item Unchanged @var{filename} \n +Response expected: no. Tell the server that @var{filename} has not been +modified in the checked out directory. The @var{filename} is +a file within the most recent directory sent with @code{Directory}; it +must not contain @samp{/}. + +@item UseUnchanged \n +Response expected: no. To specify the version of the protocol described +in this document, servers must support this request (although it need +not do anything) and clients must issue it. + +@item Notify @var{filename} \n +Response expected: no. +Tell the server that an @code{edit} or @code{unedit} command has taken +place. The server needs to send a @code{Notified} response, but such +response is deferred until the next time that the server is sending +responses. +The @var{filename} is a file within the most recent directory sent with +@code{Directory}; it must not contain @samp{/}. +Additional data: +@example +@var{notification-type} \t @var{time} \t @var{clienthost} \t +@var{working-dir} \t @var{watches} \n +@end example +where @var{notification-type} is @samp{E} for edit, @samp{U} for +unedit, undefined behavior if @samp{C}, and all other letters should be +silently ignored for future expansion. +@var{time} is the time at which the edit or unedit took place, in a +user-readable format of the client's choice (the server should treat the +time as an opaque string rather than interpreting it). +@c Might be useful to specify a format, but I don't know if we want to +@c specify the status quo (ISO C asctime() format plus timezone) without +@c offering the option of ISO8601 and/or RFC822/1123 (see cvs.texinfo +@c for much much more on date formats). +@var{clienthost} is the name of the host on which the edit or unedit +took place, and @var{working-dir} is the pathname of the working +directory where the edit or unedit took place. @var{watches} are the +temporary watches, zero or more of the following characters in the +following order: @samp{E} for edit, @samp{U} for unedit, @samp{C} for +commit, and all other letters should be silently ignored for future +expansion. If @var{notification-type} is @samp{E} the temporary watches +are set; if it is @samp{U} they are cleared. +If @var{watches} is followed by \t then the +\t and the rest of the line should be ignored, for future expansion. + +The @var{time}, @var{clienthost}, and @var{working-dir} fields may not +contain the characters @samp{+}, @samp{,}, @samp{>}, @samp{;}, or @samp{=}. + +Note that a client may be capable of performing an @code{edit} or +@code{unedit} operation without connecting to the server at that time, +and instead connecting to the server when it is convenient (for example, +when a laptop is on the net again) to send the @code{Notify} requests. +Even if a client is capable of deferring notifications, it should +attempt to send them immediately (one can send @code{Notify} requests +together with a @code{noop} request, for example), unless perhaps if +it can know that a connection would be impossible. + +@item Questionable @var{filename} \n +Response expected: no. Additional data: no. Tell the server to check +whether @var{filename} should be ignored, and if not, next time the +server sends responses, send (in a @code{M} response) @samp{?} followed +by the directory and filename. @var{filename} must not contain +@samp{/}; it needs to be a file in the directory named by the most +recent @code{Directory} request. +@c FIXME: the bit about not containing / is true of most of the +@c requests, but isn't documented and should be. + +@item Case \n +Response expected: no. Tell the server that filenames should be matched +in a case-insensitive fashion. Note that this is not the primary +mechanism for achieving case-insensitivity; for the most part the client +keeps track of the case which the server wants to use and takes care to +always use that case regardless of what the user specifies. For example +the filenames given in @code{Entry} and @code{Modified} requests for the +same file must match in case regardless of whether the @code{Case} +request is sent. The latter mechanism is more general (it could also be +used for 8.3 filenames, VMS filenames with more than one @samp{.}, and +any other situation in which there is a predictable mapping between +filenames in the working directory and filenames in the protocol), but +there are some situations it cannot handle (ignore patterns, or +situations where the user specifies a filename and the client does not +know about that file). + +@item Argument @var{text} \n +Response expected: no. +Save argument for use in a subsequent command. Arguments +accumulate until an argument-using command is given, at which point +they are forgotten. + +@item Argumentx @var{text} \n +Response expected: no. Append \n followed by text to the current +argument being saved. + +@item Global_option @var{option} \n +Response expected: no. +Transmit one of the global options @samp{-q}, @samp{-Q}, @samp{-l}, +@samp{-t}, @samp{-r}, or @samp{-n}. @var{option} must be one of those +strings, no variations (such as combining of options) are allowed. For +graceful handling of @code{valid-requests}, it is probably better to +make new global options separate requests, rather than trying to add +them to this request. + +@item Gzip-stream @var{level} \n +Response expected: no. +Use zlib (RFC 1950/1951) compression to compress all further communication +between the client and the server. After this request is sent, all +further communication must be compressed. All further data received +from the server will also be compressed. The @var{level} argument +suggests to the server the level of compression that it should apply; it +should be an integer between 1 and 9, inclusive, where a higher number +indicates more compression. + +@item Kerberos-encrypt \n +Response expected: no. +Use Kerberos encryption to encrypt all further communication between the +client and the server. This will only work if the connection was made +over Kerberos in the first place. If both the @code{Gzip-stream} and +the @code{Kerberos-encrypt} requests are used, the +@code{Kerberos-encrypt} request should be used first. This will make +the client and server encrypt the compressed data, as opposed to +compressing the encrypted data. Encrypted data is generally +incompressible. + +Note that this request does not fully prevent an attacker from hijacking +the connection, in the sense that it does not prevent hijacking the +connection between the initial authentication and the +@code{Kerberos-encrypt} request. + +@item Gssapi-encrypt \n +Response expected: no. +Use GSSAPI encryption to encrypt all further communication between the +client and the server. This will only work if the connection was made +over GSSAPI in the first place. See @code{Kerberos-encrypt}, above, for +the relation between @code{Gssapi-encrypt} and @code{Gzip-stream}. + +Note that this request does not fully prevent an attacker from hijacking +the connection, in the sense that it does not prevent hijacking the +connection between the initial authentication and the +@code{Gssapi-encrypt} request. + +@item Gssapi-authenticate \n +Response expected: no. +Use GSSAPI authentication to authenticate all further communication +between the client and the server. This will only work if the +connection was made over GSSAPI in the first place. Encrypted data is +automatically authenticated, so using both @code{Gssapi-authenticate} +and @code{Gssapi-encrypt} has no effect beyond that of +@code{Gssapi-encrypt}. Unlike encrypted data, it is reasonable to +compress authenticated data. + +Note that this request does not fully prevent an attacker from hijacking +the connection, in the sense that it does not prevent hijacking the +connection between the initial authentication and the +@code{Gssapi-authenticate} request. + +@item Set @var{variable}=@var{value} \n +Response expected: no. +Set a user variable @var{variable} to @var{value}. + +@item expand-modules \n +Response expected: yes. Expand the modules which are specified in the +arguments. Returns the data in @code{Module-expansion} responses. Note +that the server can assume that this is checkout or export, not rtag or +rdiff; the latter do not access the working directory and thus have no +need to expand modules on the client side. + +Expand may not be the best word for what this request does. It does not +necessarily tell you all the files contained in a module, for example. +Basically it is a way of telling you which working directories the +server needs to know about in order to handle a checkout of the +specified modules. + +For example, suppose that the server has a module defined by + +@example +aliasmodule -a 1dir +@end example + +That is, one can check out @code{aliasmodule} and it will take +@code{1dir} in the repository and check it out to @code{1dir} in the +working directory. Now suppose the client already has this module +checked out and is planning on using the @code{co} request to update it. +Without using @code{expand-modules}, the client would have two bad +choices: it could either send information about @emph{all} working +directories under the current directory, which could be unnecessarily +slow, or it could be ignorant of the fact that @code{aliasmodule} stands +for @code{1dir}, and neglect to send information for @code{1dir}, which +would lead to incorrect operation. +@c Those don't really seem like the only two options. I mean, what +@c about keeping track of the correspondence from when we first checked +@c out a fresh directory? Not that the CVS client does this, or that +@c I've really thought about whether it would be a good idea... + +With @code{expand-modules}, the client would first ask for the module to +be expanded: + +@example +C: Root /home/kingdon/zwork/cvsroot +. . . +C: Argument aliasmodule +C: Directory . +C: /home/kingdon/zwork/cvsroot +C: expand-modules +S: Module-expansion 1dir +S: ok +@end example + +and then it knows to check the @file{1dir} directory and send +requests such as @code{Entry} and @code{Modified} for the files in that +directory. + +@item ci \n +@itemx diff \n +@itemx tag \n +@itemx status \n +@itemx admin \n +@itemx history \n +@itemx watchers \n +@itemx editors \n +@itemx annotate \n +Response expected: yes. Actually do a cvs command. This uses any +previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. The +last @code{Directory} sent specifies the working directory at the time +of the operation. No provision is made for any input from the user. +This means that @code{ci} must use a @code{-m} argument if it wants to +specify a log message. + +@item log \n +Response expected: yes. Show information for past revisions. This uses +any previous @code{Directory}, @code{Entry}, or @code{Modified} +requests, if they have been sent. The last @code{Directory} sent +specifies the working directory at the time of the operation. Also uses +previous @code{Argument}'s of which the canonical forms are the +following (@sc{cvs} 1.10 and older clients sent what the user specified, +but clients are encouraged to use the canonical forms and other forms +are deprecated): + +@table @code +@item -b, -h, -l, -N, -R, -t +These options go by themselves, one option per @code{Argument} request. + +@item -d @var{date1}<@var{date2} +Select revisions between @var{date1} and @var{date2}. Either date +may be omitted in which case there is no date limit at that end of the +range (clients may specify dates such as 1 Jan 1970 or 1 Jan 2038 for +similar purposes but this is problematic as it makes assumptions about +what dates the server supports). Dates are in RFC822/1123 format. The +@samp{-d} is one @code{Argument} request and the date range is a second +one. + +@item -d @var{date1}<=@var{date2} +Likewise but compare dates for equality. + +@item -d @var{singledate} +Select the single, latest revision dated @var{singledate} or earlier. + +To include several date ranges and/or singledates, repeat the @samp{-d} +option as many times as necessary. + +@item -r@var{rev1}:@var{rev2} +@itemx -r@var{branch} +@itemx -r@var{branch}. +@itemx -r +Specify revisions (note that @var{rev1} or @var{rev2} can be omitted, or +can refer to branches). Send both the @samp{-r} and the revision +information in a single @code{Argument} request. To include several +revision selections, repeat the @samp{-r} option. + +@item -s @var{state} +@itemx -w +@itemx -w@var{login} +Select on states or users. To include more than one state or user, +repeat the option. Send the @samp{-s} option as a separate argument +from the state being selected. Send the @samp{-w} option as part of the +same argument as the user being selected. +@end table + +@item co \n +Response expected: yes. Get files from the repository. This uses any +previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. Arguments to this +command are module names; the client cannot know what directories they +correspond to except by (1) just sending the @code{co} request, and then +seeing what directory names the server sends back in its responses, and +(2) the @code{expand-modules} request. + +@item export \n +Response expected: yes. Get files from the repository. This uses any +previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. Arguments to this +command are module names, as described for the @code{co} request. The +intention behind this command is that a client can get sources from a +server without storing CVS information about those sources. That is, a +client probably should not count on being able to take the entries line +returned in the @code{Created} response from an @code{export} request +and send it in a future @code{Entry} request. Note that the entries +line in the @code{Created} response must indicate whether the file is +binary or text, so the client can create it correctly. + +@item rdiff \n +@itemx rtag \n +Response expected: yes. Actually do a cvs command. This uses any +previous @code{Argument} requests, if they have been sent. The client +should not send @code{Directory}, @code{Entry}, or @code{Modified} +requests for this command; they are not used. Arguments to these +commands are module names, as described for @code{co}. + +@item init @var{root-name} \n +Response expected: yes. If it doesn't already exist, create a @sc{cvs} +repository @var{root-name}. Note that @var{root-name} is a local +directory and @emph{not} a fully qualified @code{CVSROOT} variable. The +@code{Root} request need not have been previously sent. + +@item update \n +Response expected: yes. Actually do a @code{cvs update} command. This +uses any previous @code{Argument}, @code{Directory}, @code{Entry}, +or @code{Modified} requests, if they have been sent. The +last @code{Directory} sent specifies the working directory at the time +of the operation. The @code{-I} option is not used--files which the +client can decide whether to ignore are not mentioned and the client +sends the @code{Questionable} request for others. + +@item import \n +Response expected: yes. Actually do a @code{cvs import} command. This +uses any previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. The +last @code{Directory} sent specifies the working directory at the time +of the operation - unlike most commands, the repository field of each +@code{Directory} request is ignored (it merely must point somewhere +within the root). The files to be imported are sent in @code{Modified} +requests (files which the client knows should be ignored are not sent; +the server must still process the CVSROOT/cvsignore file unless -I ! is +sent). A log message must have been specified with a @code{-m} +argument. + +@item add \n +Response expected: yes. Add a file or directory. This uses any +previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. The +last @code{Directory} sent specifies the working directory at the time +of the operation. + +To add a directory, send the directory to be added using +@code{Directory} and @code{Argument} requests. For example: + +@example +C: Root /u/cvsroot +. . . +C: Argument nsdir +C: Directory nsdir +C: /u/cvsroot/1dir/nsdir +C: Directory . +C: /u/cvsroot/1dir +C: add +S: M Directory /u/cvsroot/1dir/nsdir added to the repository +S: ok +@end example + +You will notice that the server does not signal to the client in any +particular way that the directory has been successfully added. The +client is supposed to just assume that the directory has been added and +update its records accordingly. Note also that adding a directory is +immediate; it does not wait until a @code{ci} request as files do. + +To add a file, send the file to be added using a @code{Modified} +request. For example: + +@example +C: Argument nfile +C: Directory . +C: /u/cvsroot/1dir +C: Modified nfile +C: u=rw,g=r,o=r +C: 6 +C: hello +C: add +S: E cvs server: scheduling file `nfile' for addition +S: Mode u=rw,g=r,o=r +S: Checked-in ./ +S: /u/cvsroot/1dir/nfile +S: /nfile/0/// +S: E cvs server: use 'cvs commit' to add this file permanently +S: ok +@end example + +Note that the file has not been added to the repository; the only effect +of a successful @code{add} request, for a file, is to supply the client +with a new entries line containing @samp{0} to indicate an added file. +In fact, the client probably could perform this operation without +contacting the server, although using @code{add} does cause the server +to perform a few more checks. + +The client sends a subsequent @code{ci} to actually add the file to the +repository. + +Another quirk of the @code{add} request is that with CVS 1.9 and older, +a pathname specified in +an @code{Argument} request cannot contain @samp{/}. There is no good +reason for this restriction, and in fact more recent CVS servers don't +have it. +But the way to interoperate with the older servers is to ensure that +all @code{Directory} requests for @code{add} (except those used to add +directories, as described above), use @samp{.} for +@var{local-directory}. Specifying another string for +@var{local-directory} may not get an error, but it will get you strange +@code{Checked-in} responses from the buggy servers. + +@item remove \n +Response expected: yes. Remove a file. This uses any +previous @code{Argument}, @code{Directory}, @code{Entry}, or +@code{Modified} requests, if they have been sent. The +last @code{Directory} sent specifies the working directory at the time +of the operation. + +Note that this request does not actually do anything to the repository; +the only effect of a successful @code{remove} request is to supply the +client with a new entries line containing @samp{-} to indicate a removed +file. In fact, the client probably could perform this operation without +contacting the server, although using @code{remove} may cause the server +to perform a few more checks. + +The client sends a subsequent @code{ci} request to actually record the +removal in the repository. + +@item watch-on \n +@itemx watch-off \n +@itemx watch-add \n +@itemx watch-remove \n +Response expected: yes. Actually do the @code{cvs watch on}, @code{cvs +watch off}, @code{cvs watch add}, and @code{cvs watch remove} commands, +respectively. This uses any previous @code{Argument}, +@code{Directory}, @code{Entry}, or @code{Modified} +requests, if they have been sent. The last @code{Directory} sent +specifies the working directory at the time of the operation. + +@item release \n +Response expected: yes. Note that a @code{cvs release} command has +taken place and update the history file accordingly. + +@item noop \n +Response expected: yes. This request is a null command in the sense +that it doesn't do anything, but merely (as with any other requests +expecting a response) sends back any responses pertaining to pending +errors, pending @code{Notified} responses, etc. + +@item update-patches \n +Response expected: yes. +This request does not actually do anything. It is used as a signal that +the server is able to generate patches when given an @code{update} +request. The client must issue the @code{-u} argument to @code{update} +in order to receive patches. + +@item gzip-file-contents @var{level} \n +Response expected: no. Note that this request does not follow the +response convention stated above. @code{Gzip-stream} is suggested +instead of @code{gzip-file-contents} as it gives better compression; the +only reason to implement the latter is to provide compression with +@sc{cvs} 1.8 and earlier. The @code{gzip-file-contents} request asks +the server to compress files it sends to the client using @code{gzip} +(RFC1952/1951) compression, using the specified level of compression. +If this request is not made, the server must not compress files. + +This is only a hint to the server. It may still decide (for example, in +the case of very small files, or files that already appear to be +compressed) not to do the compression. Compression is indicated by a +@samp{z} preceding the file length. + +Availability of this request in the server indicates to the client that +it may compress files sent to the server, regardless of whether the +client actually uses this request. + +@item wrapper-sendme-rcsOptions \n +Response expected: yes. +Request that the server transmit mappings from filenames to keyword +expansion modes in @code{Wrapper-rcsOption} responses. + +@item @var{other-request} @var{text} \n +Response expected: yes. +Any unrecognized request expects a response, and does not +contain any additional data. The response will normally be something like +@samp{error unrecognized request}, but it could be a different error if +a previous request which doesn't expect a response produced an error. +@end table + +When the client is done, it drops the connection. + +@node Response intro +@section Introduction to Responses + +After a command which expects a response, the server sends however many +of the following responses are appropriate. The server should not send +data at other times (the current implementation may violate this +principle in a few minor places, where the server is printing an error +message and exiting---this should be investigated further). + +Any set of responses always ends with @samp{error} or @samp{ok}. This +indicates that the response is over. + +@c "file updating response" and "file update modifying response" are +@c lame terms (mostly because they are so awkward). Any better ideas? +The responses @code{Checked-in}, @code{New-entry}, @code{Updated}, +@code{Created}, @code{Update-existing}, @code{Merged}, and +@code{Patched} are refered to as @dfn{file updating} responses, because +they change the status of a file in the working directory in some way. +The responses @code{Mode}, @code{Mod-time}, and @code{Checksum} are +referred to as @dfn{file update modifying} responses because they modify +the next file updating response. In no case shall a file update +modifying response apply to a file updating response other than the next +one. Nor can the same file update modifying response occur twice for +a given file updating response (if servers diagnose this problem, it may +aid in detecting the case where clients send an update modifying +response without following it by a file updating response). + +@node Response pathnames +@section The "pathname" in responses + +Many of the responses contain something called @var{pathname}. +@c FIXME: should better document when the specified repository needs to +@c end in "/.". +The name is somewhat misleading; it actually indicates a pair of +pathnames. First, a local directory name +relative to the directory in which the command was given (i.e. the last +@code{Directory} before the command). Then a linefeed and a repository +name. Then +a slash and the filename (without a @samp{,v} ending). +For example, for a file @file{i386.mh} +which is in the local directory @file{gas.clean/config} and for which +the repository is @file{/rel/cvsfiles/devo/gas/config}: + +@example +gas.clean/config/ +/rel/cvsfiles/devo/gas/config/i386.mh +@end example + +If the server wants to tell the client to create a directory, then it +merely uses the directory in any response, as described above, and the +client should create the directory if it does not exist. Note that this +should only be done one directory at a time, in order to permit the +client to correctly store the repository for each directory. Servers +can use requests such as @code{Clear-sticky}, +@code{Clear-static-directory}, or any other requests, to create +directories. +@c FIXME: Need example here of how "repository" needs to be sent for +@c each directory, and cannot be correctly deduced from, say, the most +@c deeply nested directory. + +Some server +implementations may poorly distinguish between a directory which should +not exist and a directory which contains no files; in order to refrain +from creating empty directories a client should both send the @samp{-P} +option to @code{update} or @code{co}, and should also detect the case in +which the server asks to create a directory but not any files within it +(in that case the client should remove the directory or refrain from +creating it in the first place). Note that servers could clean this up +greatly by only telling the client to create directories if the +directory in question should exist, but until servers do this, clients +will need to offer the @samp{-P} behavior described above. + +@node Responses +@section Responses + +Here are the responses: + +@table @code +@item Valid-requests @var{request-list} \n +Indicate what requests the server will accept. @var{request-list} +is a space separated list of tokens. If the server supports sending +patches, it will include @samp{update-patches} in this list. The +@samp{update-patches} request does not actually do anything. + +@item Checked-in @var{pathname} \n +Additional data: New Entries line, \n. This means a file @var{pathname} +has been successfully operated on (checked in, added, etc.). name in +the Entries line is the same as the last component of @var{pathname}. + +@item New-entry @var{pathname} \n +Additional data: New Entries line, \n. Like @code{Checked-in}, but the +file is not up to date. + +@item Updated @var{pathname} \n +Additional data: New Entries line, \n, mode, \n, file transmission. A +new copy of the file is enclosed. This is used for a new revision of an +existing file, or for a new file, or for any other case in which the +local (client-side) copy of the file needs to be updated, and after +being updated it will be up to date. If any directory in pathname does +not exist, create it. This response is not used if @code{Created} and +@code{Update-existing} are supported. + +@item Created @var{pathname} \n +This is just like @code{Updated} and takes the same additional data, but +is used only if no @code{Entry}, @code{Modified}, or +@code{Unchanged} request has been sent for the file in question. The +distinction between @code{Created} and @code{Update-existing} is so +that the client can give an error message in several cases: (1) there is +a file in the working directory, but not one for which @code{Entry}, +@code{Modified}, or @code{Unchanged} was sent (for example, a file which +was ignored, or a file for which @code{Questionable} was sent), (2) +there is a file in the working directory whose name differs from the one +mentioned in @code{Created} in ways that the client is unable to use to +distinguish files. For example, the client is case-insensitive and the +names differ only in case. + +@item Update-existing @var{pathname} \n +This is just like @code{Updated} and takes the same additional data, but +is used only if a @code{Entry}, @code{Modified}, or @code{Unchanged} +request has been sent for the file in question. + +This response, or @code{Merged}, indicates that the server has +determined that it is OK to overwrite the previous contents of the file +specified by @var{pathname}. Provided that the client has correctly +sent @code{Modified} or @code{Is-modified} requests for a modified file, +and the file was not modified while CVS was running, the server can +ensure that a user's modifications are not lost. + +@item Merged @var{pathname} \n +This is just like @code{Updated} and takes the same additional data, +with the one difference that after the new copy of the file is enclosed, +it will still not be up to date. Used for the results of a merge, with +or without conflicts. + +It is useful to preserve an copy of what the file looked like before the +merge. This is basically handled by the server; before sending +@code{Merged} it will send a @code{Copy-file} response. For example, if +the file is @file{aa} and it derives from revision 1.3, the +@code{Copy-file} response will tell the client to copy @file{aa} to +@file{.#aa.1.3}. It is up to the client to decide how long to keep this +file around; traditionally clients have left it around forever, thus +letting the user clean it up as desired. But another answer, such as +until the next commit, might be preferable. + +@item Rcs-diff @var{pathname} \n +This is just like @code{Updated} and takes the same additional data, +with the one difference that instead of sending a new copy of the file, +the server sends an RCS change text. This change text is produced by +@samp{diff -n} (the GNU diff @samp{-a} option may also be used). The +client must apply this change text to the existing file. This will only +be used when the client has an exact copy of an earlier revision of a +file. This response is only used if the @code{update} command is given +the @samp{-u} argument. + +@item Patched @var{pathname} \n +This is just like @code{Rcs-diff} and takes the same additional data, +except that it sends a standard patch rather than an RCS change text. +The patch is produced by @samp{diff -c} for @sc{cvs} 1.6 and later (see +POSIX.2 for a description of this format), or @samp{diff -u} for +previous versions of @sc{cvs}; clients are encouraged to accept either +format. Like @code{Rcs-diff}, this response is only used if the +@code{update} command is given the @samp{-u} argument. + +The @code{Patched} response is deprecated in favor of the +@code{Rcs-diff} response. However, older clients (CVS 1.9 and earlier) +only support @code{Patched}. + +@item Mode @var{mode} \n +This @var{mode} applies to the next file mentioned in +@code{Checked-in}. @code{Mode} is a file update modifying response +as described in @ref{Response intro}. + +@item Mod-time @var{time} \n +Set the modification time of the next file sent to @var{time}. +@code{Mod-time} is a file update modifying response +as described in @ref{Response intro}. +The +@var{time} is in the format specified by RFC822 as modified by RFC1123. +The server may specify any timezone it chooses; clients will want to +convert that to their own timezone as appropriate. An example of this +format is: + +@example +26 May 1997 13:01:40 -0400 +@end example + +There is no requirement that the client and server clocks be +synchronized. The server just sends its recommendation for a timestamp +(based on its own clock, presumably), and the client should just believe +it (this means that the time might be in the future, for example). + +If the server does not send @code{Mod-time} for a given file, the client +should pick a modification time in the usual way (usually, just let the +operating system set the modification time to the time that the CVS +command is running). + +@item Checksum @var{checksum}\n +The @var{checksum} applies to the next file sent (that is, +@code{Checksum} is a file update modifying response +as described in @ref{Response intro}). +In the case of +@code{Patched}, the checksum applies to the file after being patched, +not to the patch itself. The client should compute the checksum itself, +after receiving the file or patch, and signal an error if the checksums +do not match. The checksum is the 128 bit MD5 checksum represented as +32 hex digits (MD5 is described in RFC1321). +This response is optional, and is only used if the +client supports it (as judged by the @code{Valid-responses} request). + +@item Copy-file @var{pathname} \n +Additional data: @var{newname} \n. Copy file @var{pathname} to +@var{newname} in the same directory where it already is. This does not +affect @code{CVS/Entries}. + +This can optionally be implemented as a rename instead of a copy. The +only use for it which currently has been identified is prior to a +@code{Merged} response as described under @code{Merged}. Clients can +probably assume that is how it is being used, if they want to worry +about things like how long to keep the @var{newname} file around. + +@item Removed @var{pathname} \n +The file has been removed from the repository (this is the case where +cvs prints @samp{file foobar.c is no longer pertinent}). + +@item Remove-entry @var{pathname} \n +The file needs its entry removed from @code{CVS/Entries}, but the file +itself is already gone (this happens in response to a @code{ci} request +which involves committing the removal of a file). + +@item Set-static-directory @var{pathname} \n +This instructs the client to set the @code{Entries.Static} flag, which +it should then send back to the server in a @code{Static-directory} +request whenever the directory is operated on. @var{pathname} ends in a +slash; its purpose is to specify a directory, not a file within a +directory. + +@item Clear-static-directory @var{pathname} \n +Like @code{Set-static-directory}, but clear, not set, the flag. + +@item Set-sticky @var{pathname} \n +Additional data: @var{tagspec} \n. Tell the client to set a sticky tag +or date, which should be supplied with the @code{Sticky} request for +future operations. @var{pathname} ends in a slash; its purpose is to +specify a directory, not a file within a directory. The client should +store @var{tagspec} and pass it back to the server as-is, to allow for +future expansion. The first character of @var{tagspec} is @samp{T} for +a tag, @samp{D} for a date, or something else for future expansion. The +remainder of @var{tagspec} contains the actual tag or date. + +@item Clear-sticky @var{pathname} \n +Clear any sticky tag or date set by @code{Set-sticky}. + +@item Template @var{pathname} \n +Additional data: file transmission (note: compressed file transmissions +are not supported). @var{pathname} ends in a slash; its purpose is to +specify a directory, not a file within a directory. Tell the client to +store the file transmission as the template log message, and then use +that template in the future when prompting the user for a log message. + +@item Set-checkin-prog @var{dir} \n +Additional data: @var{prog} \n. Tell the client to set a checkin +program, which should be supplied with the @code{Checkin-prog} request +for future operations. + +@item Set-update-prog @var{dir} \n +Additional data: @var{prog} \n. Tell the client to set an update +program, which should be supplied with the @code{Update-prog} request +for future operations. + +@item Notified @var{pathname} \n +Indicate to the client that the notification for @var{pathname} has been +done. There should be one such response for every @code{Notify} +request; if there are several @code{Notify} requests for a single file, +the requests should be processed in order; the first @code{Notified} +response pertains to the first @code{Notify} request, etc. + +@item Module-expansion @var{pathname} \n +Return a file or directory +which is included in a particular module. @var{pathname} is relative +to cvsroot, unlike most pathnames in responses. @var{pathname} should +be used to look and see whether some or all of the module exists on +the client side; it is not necessarily suitable for passing as an +argument to a @code{co} request (for example, if the modules file +contains the @samp{-d} option, it will be the directory specified with +@samp{-d}, not the name of the module). + +@item Wrapper-rcsOption @var{pattern} -k '@var{option}' \n +Transmit to the client a filename pattern which implies a certain +keyword expansion mode. The @var{pattern} is a wildcard pattern (for +example, @samp{*.exe}. The @var{option} is @samp{b} for binary, and so +on. Note that although the syntax happens to resemble the syntax in +certain CVS configuration files, it is more constrained; there must be +exactly one space between @var{pattern} and @samp{-k} and exactly one +space between @samp{-k} and @samp{'}, and no string is permitted in +place of @samp{-k} (extensions should be done with new responses, not by +extending this one, for graceful handling of @code{Valid-responses}). + +@item M @var{text} \n +A one-line message for the user. +Note that the format of @var{text} is not designed for machine parsing. +Although sometimes scripts and clients will have little choice, the +exact text which is output is subject to vary at the discretion of the +server and the example output given in this document is just that, +example output. Servers are encouraged to use the @samp{MT} response, +and future versions of this document will hopefully standardize more of +the @samp{MT} tags; see @ref{Text tags}. + +@item Mbinary \n +Additional data: file transmission (note: compressed file transmissions +are not supported). This is like @samp{M}, except the contents of the +file transmission are binary and should be copied to standard output +without translation to local text file conventions. To transmit a text +file to standard output, servers should use a series of @samp{M} requests. + +@item E @var{text} \n +Same as @code{M} but send to stderr not stdout. + +@item F \n +@c FIXME: The second sentence, defining "flush", is somewhat off the top +@c of my head. Is there some text we can steal from ANSI C or someplace +@c which is more carefully thought out? +Flush stderr. That is, make it possible for the user to see what has +been written to stderr (it is up to the implementation to decide exactly +how far it should go to ensure this). + +@item MT @var{tagname} @var{data} \n + +This response provides for tagged text. It is similar to +SGML/HTML/XML in that the data is structured and a naive application +can also make some sense of it without understanding the structure. +The syntax is not SGML-like, however, in order to fit into the CVS +protocol better and (more importantly) to make it easier to parse, +especially in a language like perl or awk. + +The @var{tagname} can have several forms. If it starts with @samp{a} +to @samp{z} or @samp{A} to @samp{Z}, then it represents tagged text. +If the implementation recognizes @var{tagname}, then it may interpret +@var{data} in some particular fashion. If the implementation does not +recognize @var{tagname}, then it should simply treat @var{data} as +text to be sent to the user (similar to an @samp{M} response). There +are two tags which are general purpose. The @samp{text} tag is +similar to an unrecognized tag in that it provides text which will +ordinarily be sent to the user. The @samp{newline} tag is used +without @var{data} and indicates that a newline will ordinarily be +sent to the user (there is no provision for embedding newlines in the +@var{data} of other tagged text responses). + +If @var{tagname} starts with @samp{+} it indicates a start tag and if +it starts with @samp{-} it indicates an end tag. The remainder of +@var{tagname} should be the same for matching start and end tags, and +tags should be nested (for example one could have tags in the +following order @code{+bold} @code{+italic} @code{text} @code{-italic} +@code{-bold} but not @code{+bold} @code{+italic} @code{text} +@code{-bold} @code{-italic}). A particular start and end tag may be +documented to constrain the tagged text responses which are valid +between them. + +Note that if @var{data} is present there will always be exactly one +space between @var{tagname} and @var{data}; if there is more than one +space, then the spaces beyond the first are part of @var{data}. + +Here is an example of some tagged text responses. Note that there is +a trailing space after @samp{Checking in} and @samp{initial revision:} +and there are two trailing spaces after @samp{<--}. Such trailing +spaces are, of course, part of @var{data}. + +@example +MT +checking-in +MT text Checking in +MT fname gz.tst +MT text ; +MT newline +MT rcsfile /home/kingdon/zwork/cvsroot/foo/gz.tst,v +MT text <-- +MT fname gz.tst +MT newline +MT text initial revision: +MT init-rev 1.1 +MT newline +MT text done +MT newline +MT -checking-in +@end example + +If the client does not support the @samp{MT} response, the same +responses might be sent as: + +@example +M Checking in gz.tst; +M /home/kingdon/zwork/cvsroot/foo/gz.tst,v <-- gz.tst +M initial revision: 1.1 +M done +@end example + +For a list of specific tags, see @ref{Text tags}. + +@item error @var{errno-code} @samp{ } @var{text} \n +The command completed with an error. @var{errno-code} is a symbolic +error code (e.g. @code{ENOENT}); if the server doesn't support this +feature, or if it's not appropriate for this particular message, it just +omits the errno-code (in that case there are two spaces after +@samp{error}). Text is an error message such as that provided by +strerror(), or any other message the server wants to use. +The @var{text} is like the @code{M} response, in the sense that it is +not particularly intended to be machine-parsed; servers may wish to +print an error message with @code{MT} responses, and then issue a +@code{error} response without @var{text} (although it should be noted +that @code{MT} currently has no way of flagging the output as intended +for standard error, the way that the @code{E} response does). + +@item ok \n +The command completed successfully. +@end table + +@node Text tags +@section Tags for the MT tagged text response + +The @code{MT} response, as described in @ref{Responses}, offers a +way for the server to send tagged text to the client. This section +describes specific tags. The intention is to update this section as +servers add new tags. + +In the following descriptions, @code{text} and @code{newline} tags are +omitted. Such tags contain information which is intended for users (or +to be discarded), and are subject to change at the whim of the server. +To avoid being vulnerable to such whim, clients should look for the tags +listed here, not @code{text}, @code{newline}, or other tags. + +The following tag means to indicate to the user that a file has been +updated. It is more or less redundant with the @code{Created} and +@code{Update-existing} responses, but we don't try to specify here +whether it occurs in exactly the same circumstances as @code{Created} +and @code{Update-existing}. The @var{name} is the pathname of the file +being updated relative to the directory in which the command is +occurring (that is, the last @code{Directory} request which is sent +before the command). + +@example +MT +updated +MT fname @var{name} +MT -updated +@end example + +The @code{importmergecmd} tag is used when doing an import which has +conflicts. The client can use it to report how to merge in the newly +imported changes. The @var{count} is the number of conflicts. The +newly imported changes can be merged by running the following command: +@smallexample +cvs checkout -j @var{tag1} -j @var{tag2} @var{repository} +@end smallexample + +@example +MT +importmergecmd +MT conflicts @var{count} +MT mergetag1 @var{tag1} +MT mergetag2 @var{tag2} +MT repository @var{repository} +MT -importmergecmd +@end example + +@node Example +@section Example + +@c The C:/S: convention is in imitation of RFC1869 (and presumably +@c other RFC's). In other formatting concerns, we might want to think +@c about whether there is an easy way to provide RFC1543 formatting +@c (without negating the advantages of texinfo), and whether we should +@c use RFC2234 BNF (I fear that would be less clear than +@c what we do now, however). Plus what about RFC2119 terminology (MUST, +@c SHOULD, &c) or ISO terminology (shall, should, or whatever they are)? +Here is an example; lines are prefixed by @samp{C: } to indicate the +client sends them or @samp{S: } to indicate the server sends them. + +The client starts by connecting, sending the root, and completing the +protocol negotiation. In actual practice the lists of valid responses +and requests would be longer. +@c The reason that we artificially shorten the lists is to avoid phony +@c line breaks. Any better solutions? +@c Other than that, this exchange is taken verbatim from the data +@c exchanged by CVS (as of Nov 1996). That is why some of the requests and +@c reponses are not quite what you would pick for pedagogical purposes. + +@example +C: Root /u/cvsroot +C: Valid-responses ok error Checked-in M E +C: valid-requests +S: Valid-requests Root Directory Entry Modified Argument Argumentx ci co +S: ok +C: UseUnchanged +@end example + +The client wants to check out the @code{supermunger} module into a fresh +working directory. Therefore it first expands the @code{supermunger} +module; this step would be omitted if the client was operating on a +directory rather than a module. +@c Why does it send Directory here? The description of expand-modules +@c doesn't really say much of anything about what use, if any, it makes of +@c Directory and similar requests sent previously. + +@example +C: Argument supermunger +C: Directory . +C: /u/cvsroot +C: expand-modules +@end example + +The server replies that the @code{supermunger} module expands to the +directory @code{supermunger} (the simplest case): + +@example +S: Module-expansion supermunger +S: ok +@end example + +The client then proceeds to check out the directory. The fact that it +sends only a single @code{Directory} request which specifies @samp{.} +for the working directory means that there is not already a +@code{supermunger} directory on the client. +@c What is -N doing here? + +@example +C: Argument -N +C: Argument supermunger +C: Directory . +C: /u/cvsroot +C: co +@end example + +The server replies with the requested files. In this example, there is +only one file, @file{mungeall.c}. The @code{Clear-sticky} and +@code{Clear-static-directory} requests are sent by the current +implementation but they have no effect because the default is for those +settings to be clear when a directory is newly created. + +@example +S: Clear-sticky supermunger/ +S: /u/cvsroot/supermunger/ +S: Clear-static-directory supermunger/ +S: /u/cvsroot/supermunger/ +S: E cvs server: Updating supermunger +S: M U supermunger/mungeall.c +S: Created supermunger/ +S: /u/cvsroot/supermunger/mungeall.c +S: /mungeall.c/1.1/// +S: u=rw,g=r,o=r +S: 26 +S: int mein () @{ abort (); @} +S: ok +@end example + +The current client implementation would break the connection here and make a +new connection for the next command. However, the protocol allows it +to keep the connection open and continue, which is what we show here. + +After the user modifies the file and instructs the client to check it +back in. The client sends arguments to specify the log message and file +to check in: + +@example +C: Argument -m +C: Argument Well, you see, it took me hours and hours to find +C: Argumentx this typo and I searched and searched and eventually +C: Argumentx had to ask John for help. +C: Argument mungeall.c +@end example + +It also sends information about the contents of the working directory, +including the new contents of the modified file. Note that the user has +changed into the @file{supermunger} directory before executing this +command; the top level directory is a user-visible concept because the +server should print filenames in @code{M} and @code{E} responses +relative to that directory. +@c We are waving our hands about the order of the requests. "Directory" +@c and "Argument" can be in any order, but this probably isn't specified +@c very well. + +@example +C: Directory . +C: /u/cvsroot/supermunger +C: Entry /mungeall.c/1.1/// +C: Modified mungeall.c +C: u=rw,g=r,o=r +C: 26 +C: int main () @{ abort (); @} +@end example + +And finally, the client issues the checkin command (which makes use of +the data just sent): + +@example +C: ci +@end example + +And the server tells the client that the checkin succeeded: + +@example +S: M Checking in mungeall.c; +S: E /u/cvsroot/supermunger/mungeall.c,v <-- mungeall.c +S: E new revision: 1.2; previous revision: 1.1 +S: E done +S: Mode u=rw,g=r,o=r +S: Checked-in ./ +S: /u/cvsroot/supermunger/mungeall.c +S: /mungeall.c/1.2/// +S: ok +@end example + +@node Requirements +@section Required versus optional parts of the protocol + +The following are part of every known implementation of the CVS protocol +(except obsolete, pre-1.5, versions of CVS) and it is considered +reasonable behavior to completely fail to work if you are connected with +an implementation which attempts to not support them. Requests: +@code{Root}, @code{Valid-responses}, @code{valid-requests}, +@code{Directory}, @code{Entry}, @code{Modified}, @code{Unchanged}, +@code{Argument}, @code{Argumentx}, @code{ci}, @code{co}, @code{update}. +Responses: @code{ok}, @code{error}, @code{Valid-requests}, +@code{Checked-in}, @code{Updated}, @code{Merged}, @code{Removed}, +@code{M}, @code{E}. + +A server need not implement @code{Repository}, but in order to interoperate +with CVS 1.5 through 1.9 it must claim to implement it (in +@code{Valid-requests}). The client will not actually send the request. + +@node Obsolete +@section Obsolete protocol elements + +This section briefly describes protocol elements which are obsolete. +There is no attempt to document them in full detail. + +There was a @code{Repository} request which was like @code{Directory} +except it only provided @var{repository}, and the local directory was +assumed to be similarly named. + +If the @code{UseUnchanged} request was not sent, there was a @code{Lost} +request which was sent to indicate that a file did not exist in the +working directory, and the meaning of sending @code{Entries} without +@code{Lost} or @code{Modified} was different. All current clients (CVS +1.5 and later) will send @code{UseUnchanged} if it is supported. + +@node Protocol Notes +@chapter Notes on the Protocol + +A number of enhancements are possible. Also see the file @sc{todo} in +the @sc{cvs} source distribution, which has further ideas concerning +various aspects of @sc{cvs}, some of which impact the protocol. +Similarly, the @code{http://www.cyclic.com} site, in particular the +@cite{Development of CVS} page. + +@itemize @bullet +@item +The @code{Modified} request could be speeded up by sending diffs rather +than entire files. The client would need some way to keep the version +of the file which was originally checked out; probably requiring the use +of "cvs edit" in this case is the most sensible course (the "cvs edit" +could be handled by a package like VC for emacs). This would also allow +local operation of @code{cvs diff} without arguments. + +@item +The fact that @code{pserver} requires an extra network turnaround in +order to perform authentication would be nice to avoid. This relates to +the issue of reporting errors; probably the clean solution is to defer +the error until the client has issued a request which expects a +response. To some extent this might relate to the next item (in terms +of how easy it is to skip a whole bunch of requests until we get to one +that expects a response). I know that the kerberos code doesn't wait in +this fashion, but that probably can cause network deadlocks and perhaps +future problems running over a transport which is more transaction +oriented than TCP. On the other hand I'm not sure it is wise to make +the client conduct a lengthy upload only to find there is an +authentication failure. + +@item +The protocol uses an extra network turnaround for protocol negotiation +(@code{valid-requests}). It might be nice to avoid this by having the +client be able to send requests and tell the server to ignore them if +they are unrecognized (different requests could produce a fatal error if +unrecognized). To do this there should be a standard syntax for +requests. For example, perhaps all future requests should be a single +line, with mechanisms analogous to @code{Argumentx}, or several requests +working together, to provide greater amounts of information. Or there +might be a standard mechanism for counted data (analogous to that used +by @code{Modified}) or continuation lines (like a generalized +@code{Argumentx}). It would be useful to compare what HTTP is planning +in this area; last I looked they were contemplating something called +Protocol Extension Protocol but I haven't looked at the relevant IETF +documents in any detail. Obviously, we want something as simple as +possible (but no simpler). + +@item +The scrambling algorithm in the CVS client and server actually support +more characters than those documented in @ref{Password scrambling}. +Someday we are going to either have to document them all (but this is +not as easy as it may look, see below), or (gradually and with adequate +process) phase out the support for other characters in the CVS +implementation. This business of having the feature partly undocumented +isn't a desirable state long-term. + +The problem with documenting other characters is that unless we know +what character set is in use, there is no way to make a password +portable from one system to another. For example, a with a circle on +top might have different encodings in different character sets. + +It @emph{almost} works to say that the client picks an arbitrary, +unknown character set (indeed, having the CVS client know what character +set the user has in mind is a hard problem otherwise), and scrambles +according to a certain octet<->octet mapping. There are two problems +with this. One is that the protocol has no way to transmit character 10 +decimal (linefeed), and the current server and clients have no way to +handle 0 decimal (NUL). This may cause problems with certain multibyte +character sets, in which octets 10 and 0 will appear in the middle of +other characters. The other problem, which is more minor and possibly +not worth worrying about, is that someone can type a password on one +system and then go to another system which uses a different encoding for +the same characters, and have their password not work. + +The restriction to the ISO646 invariant subset is the best approach for +strings which are not particularly significant to users. Passwords are +visible enough that this is somewhat doubtful as applied here. ISO646 +does, however, have the virtue (!?) of offending everyone. It is easy +to say "But the $ is right on people's keyboards! Surely we can't +forbid that". From a human factors point of view, that makes quite a +bit of sense. The contrary argument, of course, is that a with a circle +on top, or some of the characters poorly handled by Unicode, are on +@emph{someone}'s keyboard. + +@end itemize + +@bye diff --git a/gnu/dist/cvs/man/.cvsignore b/gnu/dist/cvs/man/.cvsignore new file mode 100644 index 00000000000..f3c7a7c5da6 --- /dev/null +++ b/gnu/dist/cvs/man/.cvsignore @@ -0,0 +1 @@ +Makefile diff --git a/gnu/dist/cvs/man/ChangeLog b/gnu/dist/cvs/man/ChangeLog new file mode 100644 index 00000000000..202ffedb2be --- /dev/null +++ b/gnu/dist/cvs/man/ChangeLog @@ -0,0 +1,237 @@ +2000-01-02 John P Cavanaugh <cavanaug@sr.hp.com> + + * cvs.texinfo: document new -C option to update, now that it works + both remotely and locally. + (Re-applied by Karl Fogel <kfogel@red-bean.com>.) + +1999-12-11 Karl Fogel <kfogel@red-bean.com> + + * Revert previous change -- it doesn't work remotely yet. + +1999-12-10 John P Cavanaugh <cavanaug@sr.hp.com> + + * cvs.1: document new -C option to update. + (Applied by Karl Fogel <kfogel@red-bean.com>.) + +1999-11-29 Larry Jones <larry.jones@sdrc.com> + + * cvsbug.8: Change .TH from section 1 to section 8. + +1999-11-10 Jim Kingdon <http://developer.redhat.com/> + + * cvs.1: Don't document -H as a command option; it has been a + while since that worked. + +1999-01-19 Vitaly V Fedrushkov <willy@snowyowl.csu.ac.ru> + + * Makefile.in (INSTALL_DATA): Wrong manpage permissions fixed. + +1998-06-28 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Update various items which were out of date. Mostly + these related to CVS no longer calling external RCS programs. + +Mon Jan 12 11:10:21 1998 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Refer to Cederqvist as Cederqvist not as cvs.texinfo. + Describe it in a way which hopefully makes more sense for people + who aren't getting the CVS source distribution itself. In "SEE + ALSO" section, also mention web pages. Use a period not a comma + at the end of the references to other manpages. + + * cvs.1: Remove $Id; we decided to get rid of these some time ago. + +Sun Jan 11 13:46:54 1998 Peter Wemm <peter@netplex.com.au> + + * cvs.1: Fix "the the" typo. From Wolfram Schneider + <wosch@freebsd.org> via FreeBSD sources. + +Wed Feb 12 14:20:43 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in (install): Depend on installdirs. + +Thu Jan 2 13:30:56 1997 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in, cvsbug.8: Remove "675" paragraph; + see ../ChangeLog for rationale. + +Sat Nov 30 14:49:32 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Add note at beginning about how cvs.texinfo is more + complete than this document. Without some such indication, users + have no way of knowing which document to consult. + +Tue Oct 1 14:01:28 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Revert all changes by Greg Woods since CVS 1.8.86. They + are for new features which are not appropriate at this stage of + the release process. + +Mon Sep 30 18:21:05 1996 Greg A. Woods <woods@most.weird.com> + + * cvs.1: document -D, -g, DIFFBIN, and GREPBIN. + +Mon Sep 16 23:22:16 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsbug.8: Remove references to cvsbug.el (or other aspects of + emacs interface) and cvsbug.info (or other aspects of texinfo + documentation). Neither one has ever existed. + +Wed Jul 24 19:01:35 1996 Ian Lance Taylor <ian@cygnus.com> + + * cvs.1: Document -x. + +Fri Sep 13 11:01:59 1996 Greg A. Woods <woods@clapton.seachange.com> + + * cvs.1: add description of -k and -K for rdiff. Mention that + import allows '-b 1' to import to the trunk. + +Tue May 14 10:23:59 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Add cvs.texinfo to the SEE ALSO section. Replace an out + of date list of default ignore patterns with a reference to + cvs.texinfo (if someone wants to undo this change that is OK with + me but I did it this way because it isn't clear people would + actually keep cvs.1 up to date). + +Wed Mar 13 17:06:39 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvsinit.8: Removed. + * Makefile.in, cvs.1, cvs.5: Remove references to cvsinit. + +Tue Feb 13 22:30:54 1996 Samuel Tardieu <sam@inf.enst.fr> + + * Makefile.in: Remove reference to mkmodules.1 + +Mon Feb 12 16:30:40 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1, cvsinit.8: Remove references to mkmodules, rm, sort + * cvs.5: Remove references to mkmodules. + * mkmodules.1: Removed. + +Tue Jan 30 18:32:27 1996 Jim Kingdon <kingdon@harvey.cyclic.com> + + * Makefile.in: Revise comment regarding install and installdirs. + +Tue Nov 14 15:47:44 1995 Greg A. Woods <woods@most.weird.com> + + * cvs.5: + - list filenames in alpha sort order + - describe the cvswrappers file + - describe the taginfo file + - fix cvsinit chapter number + (This is by no means complete -- it's just stuff I noticed.) + + * cvs.1: put the filenames in alpha sort order + + * cvsinit.8: remove the .pl extension from filenames + +Wed Oct 18 11:07:07 1995 Vince Demarco <vdemarco@bou.shl.com> + + * cvs.1 (Flag): Updated the CVSROOT/wrappers stuff. Everyone seems + to think this is a NEXT specific thing it isn't. + +Tue Oct 17 17:38:27 1995 Warren Jones <wjones@tc.fluke.com> + + * cvs.1: Change \. to \&. at start of line. + +Tue Oct 3 13:43:33 1995 Del <del@matra.com.au> + + * cvs.1: Updated man page for all the new features of 1.6 + (including some that were missed in 1.5 and 1.4.xx). This includes: + - -f and -z global options. + - tag -F, and -r options. + - rtag -F options + - CVSROOT/taginfo and CVSROOT/wrappers files (the latter could use a touch + up because I don't really understand how wrappers work or why anyone would + use them -- I haven't ever played with a NEXT. + - export -k option + - New environment variables CVS_IGNORE_REMOTE_ROOT, CVS_RSH, CVS_SERVER, and + CVSWRAPPERS. I left CVS_CLIENT_LOG, CVS_CLIENT_PORT, and CVS_SERVER_SLEEP + undocumented because these appear to be for testing / debugging only. + Note that TMPDIR, HOME and PATH are used as well and strictly speaking + should be documented. + - New files ~/.cvsrc and ~/.cvswrappers + +Tue Aug 15 08:13:14 1995 Karl Fogel <kfogel@floss.cyclic.com> + + * Makefile.in (MANFILES): include $MAN8FILES too, so they get + tarred up in the distribution just like anything else. + +Mon Jul 24 19:11:15 1995 James Kingdon <kingdon@harvey.cyclic.com> + + * cvs.1: Remove references to -q and -Q command options. + +Fri Jul 14 23:30:33 1995 Jim Blandy <jimb@totoro.cyclic.com> + + * Makefile.in (prefix): Don't forget to give this a value. + +Sun Jul 9 21:22:56 1995 Karl Fogel <kfogel@floss.cyclic.com> + + Greg Woods' change: + + * cvsbug.8, cvsinit.8: new files. + +Sun Jul 9 19:03:00 1995 Greg A. Woods <woods@most.weird.com> + + * cvs.1: document 'cvs status [-qQ]' + - note reference to cvsinit(8) and cvsbug(8) + (from previous local changes) + + * Makefile.in: add support for installing in man8, and new cvsbug + and cvsinit pages (from previous local changes) + +Sat May 27 08:46:00 1995 Jim Meyering (meyering@comco.com) + + * Makefile.in (Makefile): Regenerate only Makefile in current + directory when Makefile.in is out of date. Depend on ../config.status. + +Fri Apr 28 22:51:31 1995 Jim Blandy <jimb@totoro.bio.indiana.edu> + + * Makefile.in (DISTFILES): Updated. + (dist-dir): Renamed from dist; changed to work with DISTDIR + variable passed from parent. + +Fri Jul 15 12:58:14 1994 Ian Lance Taylor (ian@sanguine.cygnus.com) + + * Makefile.in (install): Do not depend upon installdirs. + +Sat Dec 18 01:23:13 1993 david d zuhn (zoo@monad.armadillo.com) + + * Makefile.in (VPATH): don't use $(srcdir), but @srcdir@ instead + +Mon Jun 14 12:20:33 1993 david d `zoo' zuhn (zoo at rtl.cygnus.com) + + * Makefile.in (install): remove parentdir support + +Mon Aug 31 01:42:43 1992 david d [zoo] zuhn (zoo at cirdan.cygnus.com) + + * Makefile.in (install): create $(man1dir) and $(man5dir) before + installing the man pages + +Wed Feb 26 18:04:40 1992 K. Richard Pixley (rich@cygnus.com) + + * Makefile.in, configure.in: removed traces of namesubdir, + -subdirs, $(subdir), $(unsubdir), some rcs triggers. Forced + copyrights to '92, changed some from Cygnus to FSF. + +Tue Dec 10 04:07:08 1991 K. Richard Pixley (rich at rtl.cygnus.com) + + * Makefile.in: infodir belongs in datadir. + +Tue Dec 10 03:59:10 1991 K. Richard Pixley (rich at cygnus.com) + + * cvs.man: small correction to an explanation of an example. + +Thu Dec 5 22:45:59 1991 K. Richard Pixley (rich at rtl.cygnus.com) + + * Makefile.in: idestdir and ddestdir go away. Added copyrights + and shift gpl to v2. Added ChangeLog if it didn't exist. docdir + and mandir now keyed off datadir by default. + +Wed Nov 27 02:46:20 1991 K. Richard Pixley (rich at sendai) + + * brought Makefile.in's up to standards.text. + + * fresh changelog. + diff --git a/gnu/dist/cvs/man/Makefile.in b/gnu/dist/cvs/man/Makefile.in new file mode 100644 index 00000000000..d92d494e77e --- /dev/null +++ b/gnu/dist/cvs/man/Makefile.in @@ -0,0 +1,91 @@ +# Makefile for GNU CVS documentation. +# Do not use this makefile directly, but only from `../Makefile'. +# Copyright (C) 1986-1992 Free Software Foundation, Inc. + +# 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, 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. + +SHELL = /bin/sh + +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +VPATH = @srcdir@ + +MAN1FILES = cvs.1 +MAN5FILES = cvs.5 +MAN8FILES = cvsbug.8 +MANFILES = $(MAN1FILES) $(MAN5FILES) $(MAN8FILES) + +DISTFILES = .cvsignore ChangeLog Makefile.in $(MANFILES) +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +prefix = @prefix@ +mandir = $(prefix)/man +man1dir = $(mandir)/man1 +man5dir = $(mandir)/man5 +man8dir = $(mandir)/man8 + +all: +.PHONY: all + +# This needs to depend on installdirs so that (for example) the +# install will not fail if $(prefix)/man/man5 does not yet exist. +install: all installdirs + for f in $(MAN1FILES); do \ + $(INSTALL_DATA) $(srcdir)/$$f $(man1dir)/$$f; \ + done + for f in $(MAN5FILES); do \ + $(INSTALL_DATA) $(srcdir)/$$f $(man5dir)/$$f; \ + done + for f in $(MAN8FILES); do \ + $(INSTALL_DATA) $(srcdir)/$$f $(man8dir)/$$f; \ + done + +installdirs: + $(SHELL) $(top_srcdir)/mkinstalldirs $(man1dir) $(man5dir) $(man8dir) + +.PHONY: install installdirs + +tags: +.PHONY: tags + +TAGS: +.PHONY: TAGS + +ls: + @true +.PHONY: ls + +clean: +.PHONY: clean + +distclean: clean + rm -f Makefile +.PHONY: distclean + +realclean: distclean +.PHONY: realclean + +dist-dir: + mkdir ${DISTDIR} + for i in ${DISTFILES}; do \ + ln $(srcdir)/$${i} ${DISTDIR}; \ + done +.PHONY: dist-dir + +subdir = man +Makefile: ../config.status Makefile.in + cd .. && CONFIG_FILES=$(subdir)/$@ CONFIG_HEADERS= ./config.status + +#../config.status: ../configure +# cd .. ; $(SHELL) config.status --recheck + +#../configure: ../configure.in +# cd $(top_srcdir) ; autoconf diff --git a/gnu/dist/cvs/man/cvs.1 b/gnu/dist/cvs/man/cvs.1 new file mode 100644 index 00000000000..050703274e2 --- /dev/null +++ b/gnu/dist/cvs/man/cvs.1 @@ -0,0 +1,2129 @@ +.de Id +.ds Rv \\$3 +.ds Dt \\$4 +.. +.TH CVS 1 "\*(Dt" +.\" Full space in nroff; half space in troff +.de SP +.if n .sp +.if t .sp .5 +.. +.\" quoted command +.de ` +.RB ` "\|\\$1\|" '\\$2 +.. +.SH "NAME" +cvs \- Concurrent Versions System +.SH "SYNOPSIS" +.TP +\fBcvs\fP [ \fIcvs_options\fP ] +.I cvs_command +[ +.I command_options +] [ +.I command_args +] +.SH "NOTE" +This manpage is a summary of some of the features of +.B cvs +but for more in-depth documentation, consult the Cederqvist manual (as +described in the SEE ALSO section of this manpage). +.SH "DESCRIPTION" +.IX "revision control system" "\fLcvs\fR" +.IX cvs "" "\fLcvs\fP \- concurrent versions system" +.IX "concurrent versions system \- \fLcvs\fP" +.IX "release control system" "cvs command" "" "\fLcvs\fP \- concurrent versions system" +.IX "source control system" "cvs command" "" "\fLcvs\fP \- concurrent versions system" +.IX revisions "cvs command" "" "\fLcvs\fP \- source control" +CVS is a version control system, which allows you to keep old versions +of files (usually source code), keep a log of who, when, and why +changes occurred, etc., like RCS or SCCS. Unlike the simpler systems, +CVS does not just operate on one file at a time or one directory at a +time, but operates on hierarchical collections of directories +consisting of version controlled files. CVS helps to manage releases +and to control the concurrent editing of source files among multiple +authors. CVS allows triggers to enable/log/control various +operations and works well over a wide area network. +.SP +.B cvs +keeps a single copy of the master sources. +This copy is called the source ``repository''; it contains all the +information to permit extracting previous software releases at any +time based on either a symbolic revision tag, or a date in the past. +.SH "ESSENTIAL COMMANDS" +.B cvs +provides a rich variety of commands (\fIcvs_command\fP in the +Synopsis), each of which often has a wealth of options, to satisfy the +many needs of source management in distributed environments. However, +you don't have to master every detail to do useful work with +.BR cvs ; +in fact, five commands are sufficient to use (and contribute to) +the source repository. +.TP +\fBcvs checkout\fP \fImodules\fP\|.\|.\|. +A necessary preliminary for most \fBcvs\fP work: creates your private +copy of the source for \fImodules\fP (named collections of source; you +can also use a path relative to the source repository here). You can +work with this copy without interfering with others' work. At least +one subdirectory level is always created. +.TP +.B cvs update +Execute this command from \fIwithin\fP your private source +directory when you wish to update your copies of source files from +changes that other developers have made to the source in the +repository. +.TP +\fBcvs add\fP \fIfile\fP\|.\|.\|. +Use this command to enroll new files in \fBcvs\fP records of your +working directory. The files will be added to the repository the next +time you run +.` "cvs commit". +Note: +You should use the +.` "cvs import" +command to bootstrap new sources into the source repository. +.` "cvs add" +is only used for new files to an already checked-out module. +.TP +\fBcvs remove\fP \fIfile\fP\|.\|.\|. +Use this command (after erasing any files listed) to declare that you +wish to eliminate files from the repository. The removal does not +affect others until you run +.` "cvs commit". +.TP +\fBcvs commit\fP \fIfile\fP\|.\|.\|. +Use this command when you wish to ``publish'' your changes to other +developers, by incorporating them in the source repository. +.SH "OPTIONS" +The +.B cvs +command line can include +.IR cvs_options , +which apply to the overall +.B cvs +program; a +.IR cvs_command , +which specifies a particular action on the source repository; and +.I command_options +and +.I command_arguments +to fully specify what the +.I cvs_command +will do. +.SP +.I Warning: +you must be careful of precisely where you place options relative to the +.IR cvs_command . +The same option can mean different things depending on whether it +is in the +.I cvs_options +position (to the left of a +.B cvs +command) or in the +.I command_options +position (to the right of a +.B cvs +command). +.SP +There are only two situations where you may omit +.IR cvs_command : +.` "cvs \-H" +or +.` "cvs --help" +elicits a list of available commands, and +.` "cvs \-v" +or +.` "cvs --version" +displays version information on \fBcvs\fP itself. +.SP +.SH "CVS OPTIONS" +As of release 1.6, +.B cvs +supports +.SM GNU +style long options as well as short options. Only +a few long options are currently supported, these are listed in +brackets after the short options whose functions they duplicate. +.SP +Use these options to control the overall +.B cvs +program: +.TP +.B \-H [ --help ] +Display usage information about the specified +.I cvs_command +(but do not actually execute the command). If you don't specify a +command name, +.` "cvs \-H" +displays a summary of all the commands available. +.TP +.B \-Q +Causes the command to be +.I really +quiet; the command will generate output only for serious problems. +.TP +.B \-q +Causes the command to be somewhat quiet; informational messages, such +as reports of recursion through subdirectories, are suppressed. +.TP +\fB\-b\fP \fIbindir\fP +Use +.I bindir +as the directory where +.SM RCS +programs are located (CVS 1.9 and older). +Overrides the setting of the +.SM RCSBIN +environment variable. +This value should be specified as an absolute pathname. +.TP +\fB\-d\fP \fICVS_root_directory\fP +Use +.I CVS_root_directory +as the root directory pathname of the master +source repository. +Overrides the setting of the +.SM CVSROOT +environment variable. +This value should be specified as an absolute pathname. +.TP +\fB\-e\fP \fIeditor\fP +Use +.I editor +to enter revision log information. +Overrides the setting of the +.SM CVSEDITOR +and the +.SM EDITOR +environment variables. +.TP +.B \-f +Do not read the +.B cvs +startup file (\fI~/.cvsrc\fP). +.TP +.B \-l +Do not log the +.I cvs_command +in the command history (but execute it anyway). See the description +of the +.B history +command for information on command history. +.TP +.B \-n +Do not change any files. Attempt to execute the +.IR cvs_command , +but only to issue reports; do not remove, update, or merge any +existing files, or create any new files. +.TP +.B \-t +Trace program execution; display messages showing the steps of +.B cvs +activity. Particularly useful with +.B \-n +to explore the potential impact of an unfamiliar command. +.TP +.B \-r +Makes new working files read-only. +Same effect as if the +.SM CVSREAD +environment variable is set. +.TP +.B \-v [ --version ] +Displays version and copyright information for +.BR cvs . +.TP +.B \-w +Makes new working files read-write (default). +Overrides the setting of the +.SM CVSREAD +environment variable. +.TP +.B \-x +Encrypt all communication between the client and the server. As of +this writing, this is only implemented when using a Kerberos +connection. +.TP +\fB\-z\fP \fIcompression\-level\fP +When transferring files across the network use +.B gzip +with compression level \fIcompression\-level\fP to compress and +de-compress data as it is transferred. Requires the presence of +the +.SM GNU +.B gzip +program in the current search path at both ends of the link. +.SH "USAGE" +Except when requesting general help with +.` "cvs \-H", +you must specify a +.I cvs_command +to +.B cvs +to select a specific release control function to perform. +Each +.B cvs +command accepts its own collection of options and arguments. +However, many options are available across several commands. +You can display a usage summary for each command by specifying the +.B \-H +option with the command. +.SH "CVS STARTUP FILE" +Normally, when CVS starts up, it reads the +.I .cvsrc +file from the home directory of the user reading it. This startup +procedure can be turned off with the +.B \-f +flag. +.SP +The +.I .cvsrc +file lists CVS commands with a list of arguments, one command per +line. For example, the following line in \fI.cvsrc\fP: +.SP +diff \-c +.SP +will mean that the +.` "cvs diff" +command will always be passed the \-c option in addition to any +other options that are specified in the command line (in this case +it will have the effect of producing context sensitive diffs for +all executions of +.` "cvs diff" +). +.SH "CVS COMMAND SUMMARY" +Here are brief descriptions of all the +.B cvs +commands: +.TP +.B add +Add a new file or directory to the repository, pending a +.` "cvs commit" +on the same file. +Can only be done from within sources created by a previous +.` "cvs checkout" +invocation. +Use +.` "cvs import" +to place whole new hierarchies of sources under +.B cvs +control. +(Does not directly affect repository; changes +working directory.) +.TP +.B admin +Execute +control functions on the source repository. (Changes +repository directly; uses working directory without changing it.) +.TP +.B checkout +Make a working directory of source files for editing. (Creates or changes +working directory.) +.TP +.B commit +Apply to the source repository changes, additions, and deletions from your +working directory. (Changes repository.) +.TP +.B diff +Show differences between files in working directory and source +repository, or between two revisions in source repository. +(Does not change either repository or working directory.) +.TP +.B export +Prepare copies of a set of source files for shipment off site. +Differs from +.` "cvs checkout" +in that no +.B cvs +administrative directories are created (and therefore +.` "cvs commit" +cannot be executed from a directory prepared with +.` "cvs export"), +and a symbolic tag must be specified. +(Does not change repository; creates directory similar to working +directories). +.TP +.B history +Show reports on +.B cvs +commands that you or others have executed on a particular file or +directory in the source repository. (Does not change repository or +working directory.) History logs are kept only if enabled by creation +of the +.` "$CVSROOT/CVSROOT/history" +file; see +.BR cvs ( 5 ). +.TP +.B import +Incorporate a set of updates from off-site into the source repository, +as a ``vendor branch''. (Changes repository.) +.TP ++.B init ++Initialize a repository by adding the CVSROOT subdirectory and some default ++control files. You must use this command or initialize the repository in ++some other way before you can use it. ++.TP +.B log +Display +log information. +(Does not change repository or working directory.) +.TP +.B rdiff +Prepare a collection of diffs as a patch file between two releases in +the repository. (Does not change repository or working directory.) +.TP +.B release +Cancel a +.` "cvs checkout", +abandoning any changes. +(Can delete working directory; no effect on repository.) +.TP +.B remove +Remove files from the source repository, pending a +.` "cvs commit" +on the same files. (Does not directly affect repository; +changes working directory.) +.TP +.B rtag +Explicitly specify a symbolic tag for particular revisions of files in the +source repository. See also +.` "cvs tag". +(Changes repository directly; does not require or affect +working directory.) +.TP +.B status +Show current status of files: latest version, version in working +directory, whether working version has been edited and, optionally, +symbolic tags in the +.SM RCS +file. (Does not change +repository or working directory.) +.TP +.B tag +Specify a symbolic tag for files in the repository. By default, tags +the revisions +that were last synchronized with your working directory. (Changes +repository directly; uses working directory without changing it.) +.TP +.B update +Bring your working directory up to date with changes from the +repository. Merges are performed automatically when possible; a +warning is issued if manual resolution is required for conflicting +changes. (Changes working directory; does not change repository.) +.SH "COMMON COMMAND OPTIONS" +This section describes the +.I command_options +that are available across several +.B cvs +commands. Not all commands support all of these options; each option +is only supported for commands where it makes sense. However, when +a command has one of these options you can count on the same meaning +for the option as in other commands. (Other command +options, which are listed with the individual commands, may have +different meanings from one +.B cvs +command to another.) +.I "Warning:" +the +.B history +command is an exception; +it supports many options that conflict +even with these standard options. +.TP +\fB\-D\fP \fIdate_spec\fP +Use the most recent revision no later than \fIdate_spec\fP (a single +argument, date description specifying a date in the +past). A wide variety of date formats are supported, in particular +ISO ("1972-09-24 20:05") or Internet ("24 Sep 1972 20:05"). +The \fIdate_spec\fP is interpreted as being in the local timezone, unless a +specific timezone is specified. +The specification is ``sticky'' when you use it to make a +private copy of a source file; that is, when you get a working file +using \fB\-D\fP, \fBcvs\fP records the date you +specified, so that further updates in the same directory will use the +same date (unless you explicitly override it; see the description of +the \fBupdate\fP command). +.B \-D +is available with the +.BR checkout ", " diff ", " history ", " export ", " +.BR rdiff ", " rtag ", and " +.B update +commands. +Examples of valid date specifications include: +.in +1i +.ft B +.nf +1 month ago +2 hours ago +400000 seconds ago +last year +last Monday +yesterday +a fortnight ago +3/31/92 10:00:07 PST +January 23, 1987 10:05pm +22:00 GMT +.fi +.ft P +.in -1i +.TP +.B \-f +When you specify a particular date or tag to \fBcvs\fP commands, they +normally ignore files that do not contain the tag (or did not exist on +the date) that you specified. Use the \fB\-f\fP option if you want +files retrieved even when there is no match for the tag or date. (The +most recent version is used in this situation.) +.B \-f +is available with these commands: +.BR checkout ", " export ", " +.BR rdiff ", " rtag ", and " update . +.TP +\fB\-k\fP \fIkflag\fP +Alter the default +processing of keywords. +The \fB\-k\fP option is available with the +.BR add ", " checkout ", " diff ", " export ", " +.BR rdiff ", and " update +commands. Your \fIkflag\fP specification is ``sticky'' when you use +it to create a private copy of a source file; that is, when you use +this option with the \fBcheckout\fP or \fBupdate\fP commands, +\fBcvs\fP associates your selected \fIkflag\fP with the file, and +continues to use it with future \fBupdate\fP commands on the same file +until you specify otherwise. +.SP +Some of the more useful \fIkflag\fPs are \-ko and \-kb (for binary files), +and \-kv which is useful for an +.B export +where you wish to retain keyword information after an +.B import +at some other site. +.TP +.B \-l +Local; run only in current working directory, rather than recurring through +subdirectories. Available with the following commands: +.BR checkout ", " commit ", " diff ", " +.BR export ", " remove ", " rdiff ", " rtag ", " +.BR status ", " tag ", and " update . +.I Warning: +this is not the same +as the overall +.` "cvs \-l" +option, which you can specify to the +.I left +of a +.B cvs +command! +.TP +.B \-n +Do +.I not +run any +.BR checkout / commit / tag / update +program. (A program can be specified to run on each of these +activities, in the modules database; this option bypasses it.) +Available with the +.BR checkout ", " commit ", " export ", and " +.B rtag +commands. +.I Warning: +this is not the same +as the overall +.` "cvs \-n" +option, which you can specify to the +.I left +of a +.B cvs +command! +.TP +.B \-P +Prune (remove) directories that are empty after being updated, on +.BR checkout ", or " update . +Normally, an empty directory (one that is void of revision-controlled +files) is left alone. +Specifying +.B \-P +will cause these directories to be silently removed from your checked-out +sources. +This does not remove the directory from the repository, only from your +checked out copy. +Note that this option is implied by the +.B \-r +or +.B \-D +options of +.BR checkout " and " export . +.TP +.B \-p +Pipe the files retrieved from the repository to standard output, +rather than writing them in the current directory. Available with the +.BR checkout " and " update +commands. +.TP +\fB\-r\fP \fItag\fP +Use the revision specified by the +.I tag +argument instead of the default ``head'' revision. As well as +arbitrary tags defined with the \fBtag\fP or \fBrtag\fP command, two +special tags are always available: +.` "HEAD" +refers to the most +recent version available in the repository, and +.` "BASE" +refers to the revision you last checked out into the current working +directory. +.SP +The \fItag\fP specification is ``sticky'' when you use +this option with +.` "cvs checkout" +or +.` "cvs update" +to +make your own copy of a file: \fBcvs\fP remembers the \fItag\fP and +continues to use it on future \fBupdate\fP commands, until you specify +otherwise. +.I tag +can be either a symbolic or numeric tag. +Specifying the +.B \-q +global option along with the +.B \-r +command option is often useful, to suppress the warning messages when the +.SM RCS +file does not contain the specified tag. +.B \-r +is available with the +.BR checkout ", " commit ", " diff ", " +.BR history ", " export ", " +.BR rdiff ", " rtag ", and " update +commands. +.I Warning: +this is not the same +as the overall +.` "cvs \-r" +option, which you can specify to the +.I left +of a +.B cvs +command! +.SH "CVS COMMANDS" +Here (finally) are details on all the +.B cvs +commands and the options each accepts. The summary lines at the top +of each command's description highlight three kinds of things: +.TP 1i +\ \ \ \ Command Options and Arguments +Special options are described in detail below; common command options +may appear only in the summary line. +.TP 1i +\ \ \ \ Working Directory, or Repository? +Some \fBcvs\fP commands require a working directory to operate; some +require a repository. Also, some commands \fIchange\fP the +repository, some change the working directory, and some change +nothing. +.TP 1i +\ \ \ \ Synonyms +Many commands have synonyms, which you may find easier to +remember (or type) than the principal name. +.PP +.TP +\fBadd\fP [\fB\-k\fP \fIkflag\fP] [\fB\-m '\fP\fImessage\fP\fB'\fP] \fIfiles.\|.\|.\fP +.I Requires: +repository, working directory. +.br +.I Changes: +working directory. +.br +.I Synonym: +.B new +.br +Use the +.B add +command to create a new file or directory in the +source repository. +The files or directories specified with +.B add +must already exist in the current directory (which must have been created +with the +.B checkout +command). +To add a whole new directory hierarchy to the source repository +(for example, files received from a third-party vendor), use the +.` "cvs import" +command instead. +.SP +If the argument to +.` "cvs add" +refers to an immediate sub-directory, the directory is +created at the correct place in the +source repository, and the necessary +.B cvs +administration files are created in your working directory. +If the directory already exists in the source repository, +.` "cvs add" +still creates the administration files in your version of the directory. +This allows you to use +.` "cvs add" +to add a particular directory to your private sources even if +someone else created that directory after your +.B checkout +of the sources. You can do the following: +.SP +.in +1i +.ft B +.nf +example% mkdir new_directory +example% cvs add new_directory +example% cvs update new_directory +.fi +.ft P +.in -1i +.SP +An alternate approach using +.` "cvs update" +might be: +.SP +.in +1i +.ft B +.nf +example% cvs update -d new_directory +.fi +.ft P +.in -1i +.SP +(To add \fIany available\fP new directories to your working directory, it's +probably simpler to use +.` "cvs checkout" +or +.` "cvs update -d".) +.SP +The added files are not placed in the +source repository until you use +.` "cvs commit" +to make the change permanent. +Doing a +.` "cvs add" +on a file that was removed with the +.` "cvs remove" +command will resurrect the file, if no +.` "cvs commit" +command intervened. +.SP +You will have the opportunity to specify a logging message, as usual, +when you use +.` "cvs commit" +to make the new file permanent. If you'd like to have another +logging message associated with just +.I creation +of the file (for example, to describe the file's purpose), you can +specify it with the +.` "\-m \fImessage\fP" +option to the +.B add +command. +.SP +The +.` "-k kflag" +option specifies the default way that this +file will be checked out. +The +.` "kflag" +argument is stored in the +.SM RCS +file and can be changed with +.` "cvs admin". +Specifying +.` "-ko" +is useful for checking in binaries that +shouldn't have +keywords expanded. +.TP +\fBadmin\fP [\fIrcs-options\fP] \fIfiles.\|.\|.\fP +.I Requires: +repository, working directory. +.br +.I Changes: +repository. +.br +.I Synonym: +.B rcs +.br +This is the +.B cvs +interface to assorted administrative +facilities, similar to +.BR rcs ( 1 ). +This command works recursively, so extreme care should be +used. +.TP +\fBcheckout\fP [\fBoptions\fP] \fImodules\fP.\|.\|. +.I Requires: +repository. +.br +.I Changes: +working directory. +.br +.I Synonyms: +.BR co ", " get +.br +Make a working directory containing copies of the source files specified by +.IR modules . +You must execute +.` "cvs checkout" +before using most of the other +.B cvs +commands, since most of them operate on your working directory. +.SP +\fImodules\fP are either symbolic names (themselves defined as the +module +.` "modules" +in the source repository; see +.BR cvs ( 5 )) +for some collection of source directories and files, or paths to +directories or files in the repository. +.SP +Depending on the +.I modules +you specify, +.B checkout +may recursively create directories and populate them with the appropriate +source files. +You can then edit these source files at any time (regardless of whether +other software developers are editing their own copies of the sources); +update them to include new changes applied by others to the source +repository; or commit your work as a permanent change to the +repository. +.SP +Note that +.B checkout +is used to create directories. +The top-level directory created is always added to the directory +where +.B checkout +is invoked, and usually has the same name as the specified +.IR module . +In the case of a +.I module +alias, the created sub-directory may have a different name, but you can be +sure that it will be a sub-directory, and that +.B checkout +will show the relative path leading to each file as it is extracted into +your private work area (unless you specify the +.B \-Q +global option). +.SP +Running +.` "cvs checkout" +on a directory that was already built by a prior +.B checkout +is also permitted, and +has the same effect as specifying the +.B \-d +option to the +.B update +command described below. +.SP +The +.I options +permitted with +.` "cvs checkout" +include the standard command options +.BR \-P ", " \-f ", " +.BI \-k " kflag" +\&, +.BR \-l ", " \-n ", " \-p ", " +.BR \-r +.IR tag ", and" +.BI \-D " date"\c +\&. +.SP +In addition to those, you can use these special command options +with +.BR checkout : +.SP +Use the +.B \-A +option to reset any sticky tags, dates, or +.B \-k +options. (If you get a working file using one of the +\fB\-r\fP, \fB\-D\fP, or \fB\-k\fP options, \fBcvs\fP remembers the +corresponding tag, date, or \fIkflag\fP and continues using it on +future updates; use the \fB\-A\fP option to make \fBcvs\fP forget these +specifications, and retrieve the ``head'' version of the file). +.SP +The +.BI \-j " branch" +option merges the changes made between the +resulting revision and the revision that it is based on (e.g., if +the tag refers to a branch, +.B cvs +will merge all changes made in that branch into your working file). +.SP +With two \fB-j\fP options, +.B cvs +will merge in the changes between the two respective revisions. +This can be used to ``remove'' a certain delta from your working file. +.SP +In addition, each \fB-j\fP option can contain on optional date +specification which, when used with branches, can limit the chosen +revision to one within a specific date. +An optional date is specified by adding a colon (:) to the tag. +An example might be what +.` "cvs import" +tells you to do when you have +just imported sources that have conflicts with local changes: +.SP +.in +1i +.ft B +.nf +example% cvs checkout -jTAG:yesterday -jTAG module +.fi +.ft P +.in -1i +.SP +Use the +.B \-N +option with +.` "\-d \fIdir\fP" +to avoid shortening module paths in your working directory. (Normally, \fBcvs\fP shortens paths as much as possible when you specify an explicit target directory.) +.SP +Use the +.B \-c +option to copy the module file, sorted, to the standard output, +instead of creating or modifying any files or directories in your +working directory. +.SP +Use the +.BI \-d " dir" +option to create a directory called +.I dir +for the working files, instead of using the module name. Unless you +also use \fB\-N\fP, the paths created under \fIdir\fP will be as short +as possible. +.SP +Use the +.B \-s +option to display per-module status information stored with +the +.B \-s +option within the modules file. +.TP +\fBcommit\fP [\fB\-lnR\fP] [\fB\-m\fP '\fIlog_message\fP' | \fB\-f\fP \fIfile\fP] [\fB\-r\fP \fIrevision\fP] [\fIfiles.\|.\|.\fP] +.I Requires: +working directory, repository. +.br +.I Changes: +repository. +.br +.I Synonym: +.B ci +.br +Use +.` "cvs commit" +when you want to incorporate changes from your working source +files into the general source repository. +.SP +If you don't specify particular \fIfiles\fP to commit, all +of the files in your working current directory are examined. +.B commit +is careful to change in the repository only those files that you have +really changed. By default (or if you explicitly specify the +.B \-R +option), files +in subdirectories are also examined and committed if they have +changed; you can use the +.B \-l +option to limit +.B commit +to the current directory only. +Sometimes you may want to force a file to be committed even though it +is unchanged; this is achieved with the +.B \-f +flag, which also has the effect of disabling recursion (you can turn +it back on with +.B \-R +of course). +.SP +.B commit +verifies that the selected files are up to date with the current revisions +in the source repository; it will notify you, and exit without +committing, if any of the specified files must be made current first +with +.` "cvs update". +.B commit +does not call the +.B update +command for you, but rather leaves that for you to do when +the time is right. +.SP +When all is well, an editor is invoked to allow you to enter a log +message that will be written to one or more logging programs and placed in the +source repository file. +You can instead specify the log message on the command line with the +.B \-m +option, thus suppressing the editor invocation, or use the +.B \-F +option to specify that the argument \fIfile\fP contains the log message. +.SP +The +.B \-r +option can be used to commit to a particular symbolic or numeric revision. +For example, to bring all your files up to the +revision ``3.0'' (including those that haven't changed), you might do: +.SP +.in +1i +.ft B +.nf +example% cvs commit -r3.0 +.fi +.ft P +.in -1i +.SP +.B cvs +will only allow you to commit to a revision that is on the main trunk (a +revision with a single dot). +However, you can also commit to a branch revision (one that has an even +number of dots) with the +.B \-r +option. +To create a branch revision, one typically use the +.B \-b +option of the +.BR rtag " or " tag +commands. +Then, either +.BR checkout " or " update +can be used to base your sources on the newly created branch. +From that point on, all +.B commit +changes made within these working sources will be automatically added +to a branch revision, thereby not perturbing main-line development in any +way. +For example, if you had to create a patch to the 1.2 version of the +product, even though the 2.0 version is already under development, you +might do: +.SP +.in +1i +.ft B +.nf +example% cvs rtag -b -rFCS1_2 FCS1_2_Patch product_module +example% cvs checkout -rFCS1_2_Patch product_module +example% cd product_module +[[ hack away ]] +example% cvs commit +.fi +.ft P +.in -1i +.SP +Say you have been working on some extremely experimental software, based on +whatever revision you happened to checkout last week. +If others in your group would like to work on this software with you, but +without disturbing main-line development, you could commit your change to a +new branch. +Others can then checkout your experimental stuff and utilize the full +benefit of +.B cvs +conflict resolution. +The scenario might look like: +.SP +.in +1i +.ft B +.nf +example% cvs tag -b EXPR1 +example% cvs update -rEXPR1 +[[ hack away ]] +example% cvs commit +.fi +.ft P +.in -1i +.SP +Others would simply do +.` "cvs checkout -rEXPR1 whatever_module" +to work with you on the experimental change. +.TP +\fBdiff\fP [\fB\-kl\fP] [\fIrcsdiff_options\fP] [[\fB\-r\fP \fIrev1\fP | \fB\-D\fP \fIdate1\fP] [\fB\-r\fP \fIrev2\fP | \fB\-D\fP \fIdate2\fP]] [\fIfiles.\|.\|.\fP] +.I Requires: +working directory, repository. +.br +.I Changes: +nothing. +.br +You can compare your working files with revisions in the source +repository, with the +.` "cvs diff" +command. If you don't specify a particular revision, your files +are compared with the revisions they were based on. You can also use +the standard +.B cvs +command option +.B \-r +to specify a particular revision to compare your files with. Finally, +if you use +.B \-r +twice, you can see differences between two revisions in the +repository. +You can also specify +.B \-D +options to diff against a revision in the past. +The +.B \-r +and +.B \-D +options can be mixed together with at most two options ever specified. +.SP +See +.BR rcsdiff ( 1 ) +for a list of other accepted options. +.SP +If you don't specify any files, +.B diff +will display differences for all those files in the current directory +(and its subdirectories, unless you use the standard option +.BR \-l ) +that +differ from the corresponding revision in the source repository +(i.e. files that +.I you +have changed), or that differ from the revision specified. +.TP +\fBexport\fP [\-\fBf\|lNnQq\fP] \fB\-r\fP \fIrev\fP\||\|\fB\-D\fP \fIdate\fP [\fB\-d\fP \fIdir\fP] [\fB\-k\fP \fIkflag\fP] \fImodule\fP.\|.\|. +.I Requires: +repository. +.br +.I Changes: +current directory. +.br +This command is a variant of +.` "cvs checkout"; +use it when you want a copy of the source for \fImodule\fP +without the \fBcvs\fP administrative directories. For example, you +might use +.` "cvs export" +to prepare source for shipment +off-site. This command \fIrequires\fP that you specify a date or tag +(with \fB\-D\fP or \fB\-r\fP), so that you can count on reproducing +the source you ship to others. +.SP +The only non-standard options are +.` "\-d \fIdir\fP" +(write the +source into directory \fIdir\fP) and +.` "\-N" +(don't shorten +module paths). +These have the same meanings as the same options in +.` "cvs checkout". +.SP +The +.B \-kv +option is useful when +.B export +is used. +This causes any +keywords to be expanded such that an +.B import +done at some other site will not lose the keyword revision information. +Other \fIkflag\fPs may be used with +.` "cvs export" +and are described in +.BR co ( 1 ). +.TP +\fBhistory\fP [\fB\-\fP\fIreport\fP] [\fB\-\fP\fIflags\fP] [\fB\-\fP\fIoptions args\fP] [\fIfiles\fP.\|.\|.] +.I Requires: +the file +.` "$CVSROOT/CVSROOT/history" +.br +.I Changes: +nothing. +.br +\fBcvs\fP keeps a history file that tracks each use of the +\fBcheckout\fP, \fBcommit\fP, \fBrtag\fP, \fBupdate\fP, and \fBrelease\fP +commands. You can use +.` "cvs history" +to display this +information in various formats. +.SP +.I Warning: +.` "cvs history" +uses +.` "\-f", +.` "\-l", +.` "\-n", +and +.` "\-p" +in ways that conflict with the +descriptions in +.SM +COMMON COMMAND OPTIONS\c +\&. +.SP +Several options (shown above as \fB\-\fP\fIreport\fP) control what +kind of report is generated: +.TP 1i +.B \ \ \ \ \ \ \-c +Report on each time \fBcommit\fP was used (i.e., each time the +repository was modified). +.TP 1i +\fB\ \ \ \ \ \ \-m\fP \fImodule\fP +Report on a particular \fImodule\fP. (You can meaningfully use +\fB\-m\fP more than once on the command line.) +.TP 1i +.B \ \ \ \ \ \ \-o +Report on checked-out modules. +.TP 1i +.B \ \ \ \ \ \ \-T +Report on all tags. +.TP 1i +\fB\ \ \ \ \ \ \-x\fP \fItype\fP +Extract a particular set of record types \fIX\fP from the \fBcvs\fP +history. The types are indicated by single letters, which you may +specify in combination. +Certain commands have a single record type: \fBcheckout\fP (type `O'), +\fBrelease\fP (type `F'), and \fBrtag\fP (type `T'). One of four +record types may result from an \fBupdate\fP: `W', when the working copy +of a file is deleted during update (because it was gone from the +repository); `U', when a working file was copied from the +repository; `G', when a merge was necessary and it succeeded; and 'C', +when a merge was necessary but collisions were detected (requiring +manual merging). Finally, one of three record types results from +\fBcommit\fP: `M', when a file was modified; `A', when a file is first +added; and `R', when a file is removed. +.TP 1i +.B \ \ \ \ \ \ \-e +Everything (all record types); equivalent to specifying +.` "\-xMACFROGWUT". +.TP 1i +\fB\ \ \ \ \ \ \-z\fP \fIzone\fP +Use time zone +.I zone +when outputting history records. +The zone name +.B LT +stands for local time; +numeric offsets stand for hours and minutes ahead of UTC. +For example, +.B +0530 +stands for 5 hours and 30 minutes ahead of (i.e. east of) UTC. +.PP +.RS .5i +The options shown as \fB\-\fP\fIflags\fP constrain the report without +requiring option arguments: +.RE +.TP 1i +.B \ \ \ \ \ \ \-a +Show data for all users (the default is to show data only for the user +executing +.` "cvs history"). +.TP 1i +.B \ \ \ \ \ \ \-l +Show last modification only. +.TP 1i +.B \ \ \ \ \ \ \-w +Show only the records for modifications done from the same working +directory where +.` "cvs history" +is executing. +.PP +.RS .5i +The options shown as \fB\-\fP\fIoptions args\fP constrain the report +based on an argument: +.RE +.TP 1i +\fB\ \ \ \ \ \ \-b\fP \fIstr\fP +Show data back to a record containing the string \fIstr\fP in either +the module name, the file name, or the repository path. +.TP 1i +\fB\ \ \ \ \ \ \-D\fP \fIdate\fP +Show data since \fIdate\fP. +.TP 1i +\fB\ \ \ \ \ \ \-p\fP \fIrepository\fP +Show data for a particular source repository (you can specify several +\fB\-p\fP options on the same command line). +.TP 1i +\fB\ \ \ \ \ \ \-r\fP \fIrev\fP +Show records referring to revisions since the revision or tag +named \fIrev\fP appears in individual RCS files. +Each +.SM RCS +file is searched for the revision or tag. +.TP 1i +\fB\ \ \ \ \ \ \-t\fP \fItag\fP +Show records since tag \fItag\fP was last added to the history file. +This differs from the \fB-r\fP flag above in that it reads +only the history file, not the +.SM RCS +files, and is much faster. +.TP 1i +\fB\ \ \ \ \ \ \-u\fP \fIname\fP +Show records for user \fIname\fP. +.PP +.TP +\fBimport\fP [\fB\-\fP\fIoptions\fP] \fIrepository vendortag releasetag\fP.\|.\|. +.I Requires: +Repository, source distribution directory. +.br +.I Changes: +repository. +.br +Use +.` "cvs import" +to incorporate an entire source +distribution from an outside source (e.g., a source vendor) into your +source repository directory. You can use this command both for +initial creation of a repository, and for wholesale updates to the +module form the outside source. +.SP +The \fIrepository\fP argument gives a directory name (or a path to a +directory) under the CVS root directory for repositories; if the +directory did not exist, \fBimport\fP creates it. +.SP +When you use \fBimport\fP for updates to source that has been modified in your +source repository (since a prior \fBimport\fP), it +will notify you of any files that conflict in the two branches of +development; use +.` "cvs checkout -j" +to reconcile the differences, as \fBimport\fP instructs you to do. +.SP +By default, certain file names are ignored during +.` "cvs import": +names associated with +.SM CVS +administration, or with other common source control systems; common +names for patch files, object files, archive files, and editor backup +files; and other names that are usually artifacts of assorted utilities. +For an up to date list of ignored file names, see the Cederqvist manual (as +described in the SEE ALSO section of this manpage). +.SP +The outside source is saved in a first-level +branch, by default +.` "1.1.1". +Updates are leaves of this +branch; for example, files from the first imported collection of +source will be revision +.` "1.1.1.1", +then files from the first +imported update will be revision +.` "1.1.1.2", +and so on. +.SP +At least three arguments are required. \fIrepository\fP is needed to +identify the collection of source. \fIvendortag\fP is a tag for the +entire branch (e.g., for +.` "1.1.1"). +You must also specify at +least one \fIreleasetag\fP to identify the files at the leaves created +each time you execute +.` "cvs import". +.SP +One of the standard +.B cvs +command options is available: \fB\-m\fP +\fImessage\fP. If you do not specify a logging message with +\fB\-m\fP, your editor is invoked (as with \fBcommit\fP) to allow you +to enter one. +.SP +There are three additional special options. +.SP +Use +.` "\-d" +to specify that each file's time of last modification should be used +for the checkin date and time. +.SP +Use +.` "\-b \fIbranch\fP" +to specify a first-level branch other +than +.` "1.1.1". +.SP +Use +.` "\-I \fIname\fP" +to specify file names that should be +ignored during \fBimport\fP. You can use this option repeatedly. +To avoid ignoring any files at all (even those ignored by default), +specify +.` "\-I !". +.TP +\fBlog\fP [\fB\-l\fP] \fIrlog-options [files\fP\|.\|.\|.] +.I Requires: +repository, working directory. +.br +.I Changes: +nothing. +.br +.I Synonym: +.B rlog +.br +Display log information for \fIfiles\fP. +Among the more useful options are \fB\-h\fP +to display only the header (including tag definitions, but omitting +most of the full log); \fB\-r\fP to select logs on particular +revisions or ranges of revisions; and \fB\-d\fP to select particular +dates or date ranges. See +.BR rlog ( 1 ) +for full explanations. +This command is recursive by default, unless the +.B \-l +option is specified. +.TP +\fBrdiff\fP [\fB\-\fP\fIflags\fP] [\fB\-V\fP \fIvn\fP] [\fB\-r\fP \fIt\fP|\fB\-D\fP \fId\fP [\fB\-r\fP \fIt2\fP|\fB\-D\fP \fId2\fP]] \fImodules\|.\|.\|.\fP +.I Requires: +repository. +.br +.I Changes: +nothing. +.br +.I Synonym: +.B patch +.br +Builds a Larry Wall format +.BR patch ( 1 ) +file between two releases, that can be fed directly into the +.B patch +program to bring an old release up-to-date with the new release. +(This is one of the few \fBcvs\fP commands that operates directly from +the repository, and doesn't require a prior +.BR checkout .) +The diff output is sent to the standard output device. +You can specify (using the standard \fB\-r\fP and \fB\-D\fP options) +any combination of one or two revisions or dates. +If only one revision or date is specified, the +patch file reflects differences between that revision or date and the +current ``head'' revisions in the +.SM RCS +file. +.SP +Note that if the software release affected +is contained in more than one directory, then it may be necessary to +specify the +.B \-p +option to the +.B patch +command when patching the old sources, so that +.B patch +is able to find the files that are located in other directories. +.SP +The standard option \fIflags\fP \fB\-f\fP, and \fB\-l\fP +are available with this command. There are also several +special options flags: +.SP +If you use the +.B \-s +option, no patch output is produced. +Instead, a summary of the changed or added files between the two +releases is sent to the standard output device. +This is useful for finding out, for example, which files have changed +between two dates or revisions. +.SP +If you use the +.B \-t +option, a diff of the top two revisions is sent to the standard output device. +This is most useful for seeing what the last change to a file was. +.SP +If you use the +.B \-u +option, the patch output uses the newer ``unidiff'' format for context +diffs. +.SP +You can use +.B \-c +to explicitly specify the +.` "diff \-c" +form of context diffs +(which is the default), if you like. +.TP +\fBrelease\fP [\fB\-dQq\fP] \fImodules\fP\|.\|.\|. +.I Requires: +Working directory. +.br +.I Changes: +Working directory, history log. +.br +This command is meant to safely cancel the effect of +.` "cvs checkout'. +Since +.B cvs +doesn't lock files, it isn't strictly necessary to use this command. +You can always simply delete your working directory, if you +like; but you risk losing changes you may have forgotten, and you +leave no trace in the +.B cvs +history file that you've abandoned your checkout. +.SP +Use +.` "cvs release" +to avoid these problems. This command +checks that no un-committed changes are present; that you are +executing it from immediately above, or inside, a \fBcvs\fP working +directory; and that the repository recorded for your files is the same +as the repository defined in the module database. +.SP +If all these conditions are true, +.` "cvs release" +leaves a +record of its execution (attesting to your intentionally abandoning +your checkout) in the +.B cvs +history log. +.SP +You can use the \fB\-d\fP flag to request that your working copies of +the source files be deleted if the \fBrelease\fP succeeds. +.TP +\fBremove\fP [\fB\-lR\fP] [\fIfiles\|.\|.\|.\fP] +.I Requires: +Working directory. +.br +.I Changes: +Working directory. +.br +.I Synonyms: +.BR rm ", " delete +.br +Use this command to declare that you wish to remove \fIfiles\fP from +the source repository. Like most +.B cvs +commands, +.` "cvs remove" +works on files in your working +directory, not directly on the repository. As a safeguard, it also +requires that you first erase the specified files from your working +directory. +.SP +The files are not actually removed until you apply your changes to the +repository with +.BR commit ; +at that point, the corresponding +.SM RCS +files in the source repository are +.I moved +into the +.` "Attic" +directory (also within the source repository). +.SP +This command is recursive by default, scheduling all physically removed +files that it finds for removal by the next +.BR commit . +Use the +.B \-l +option to avoid this recursion, or just specify that actual files that you +wish remove to consider. +.TP +\fBrtag\fP [\fB\-f\|alnRQq\fP] [\fB\-b\fP] [\fB\-d\fP] [\fB\-r\fP \fItag\fP | \fB\-D\fP \fIdate\fP] \fIsymbolic_tag\fP \fImodules\|.\|.\|.\fP +.I Requires: +repository. +.br +.I Changes: +repository. +.br +.I Synonym: +.B rfreeze +.br +You can use this command to assign symbolic tags to particular, +explicitly specified source versions in the repository. +.` "cvs rtag" +works directly on the repository contents (and requires no +prior +.BR checkout ). +Use +.` "cvs tag" +instead, to base the selection of +versions to tag on the contents of your working directory. +.SP +In general, tags (often the symbolic names of software distributions) +should not be removed, but the +.B \-d +option is available as a means to remove completely obsolete symbolic names +if necessary (as might be the case for an Alpha release, say). +.SP +.` "cvs rtag" +will not move a tag that already exists. With the \fB\-F\fP option, +however, +.` "cvs rtag" +will re-locate any instance of \fIsymbolic_tag\fP that already exists +on that file to the new repository versions. Without the \fB\-F\fP +option, attempting to use +.` "cvs rtag" +to apply a tag that already exists on that file will produce an error +message. +.SP +The \fB-b\fP option makes the tag a ``branch'' tag, allowing +concurrent, isolated development. +This is most useful for creating a patch to a previously released software +distribution. +.SP +You can use the standard \fB\-r\fP and \fB\-D\fP options to tag only those +files that already contain a certain tag. This method would be used +to rename a tag: tag only the files identified by the old tag, then delete the +old tag, leaving the new tag on exactly the same files as the old tag. +.SP +.B rtag +executes recursively by default, tagging all subdirectories of +\fImodules\fP you specify in the argument. You can restrict its +operation to top-level directories with the standard \fB\-l\fP option; +or you can explicitly request recursion with \fB\-R\fP. +.SP +The modules database can specify a program to execute whenever a tag +is specified; a typical use is to send electronic mail to a group of +interested parties. If you want to bypass that program, use the +standard \fB\-n\fP option. +.SP +Use the +.B \-a +option to have +.B rtag +look in the +.` "Attic" +for removed files that contain the specified tag. +The tag is removed from these files, which makes it convenient to re-use a +symbolic tag as development continues (and files get removed from the +up-coming distribution). +.TP +\fBstatus\fP [\fB\-lRqQ\fP] [\fB\-v\fP] [\fIfiles\fP\|.\|.\|.] +.I Requires: +working directory, repository. +.br +.I Changes: +nothing. +.br +Display a brief report on the current status of \fIfiles\fP with +respect to the source repository, including any ``sticky'' tags, +dates, or \fB\-k\fP options. (``Sticky'' options will restrict how +.` "cvs update" +operates until you reset them; see the +description of +.` "cvs update \-A\|.\|.\|.".) +.SP +You can also use this command to anticipate the potential impact of a +.` "cvs update" +on your working source directory. If you do +not specify any \fIfiles\fP explicitly, reports are shown for all +files that \fBcvs\fP has placed in your working directory. You can +limit the scope of this search to the current directory itself (not +its subdirectories) with the standard \fB\-l\fP option flag; or you +can explicitly request recursive status reports with the \fB\-R\fP +option. +.SP +The +.B \-v +option causes the symbolic tags for the +.SM RCS +file to be displayed as well. +.TP +\fBtag\fP [\fB\-lQqR\fP] [\fB\-F\fP] [\fB\-b\fP] [\fB\-d\fP] [\fB\-r\fP \fItag\fP | \fB\-D\fP \fIdate\fP] [\fB\-f\fP] \fIsymbolic_tag\fP [\fIfiles\fP\|.\|.\|.\|] +.I Requires: +working directory, repository. +.br +.I Changes: +repository. +.br +.I Synonym: +.B freeze +.br +Use this command to assign symbolic tags to the nearest repository +versions to your working sources. The tags are applied immediately to +the repository, as with \fBrtag\fP. +.SP +One use for tags is to record a ``snapshot'' of the current sources +when the software freeze date of a project arrives. As bugs are fixed +after the freeze date, only those changed sources that are to be part +of the release need be re-tagged. +.SP +The symbolic tags are meant to permanently record which revisions of which +files were used in creating a software distribution. +The +.BR checkout , +.B export +and +.B update +commands allow you to extract an exact copy of a tagged release at any time in +the future, regardless of whether files have been changed, added, or removed +since the release was tagged. +.SP +You can use the standard \fB\-r\fP and \fB\-D\fP options to tag only those +files that already contain a certain tag. This method would be used +to rename a tag: tag only the files identified by the old tag, then delete the +old tag, leaving the new tag on exactly the same files as the old tag. +.SP +Specifying the \fB\-f\fP flag in addition to the \fB\-r\fP or \fB\-D\fP +flags will tag those files named on the command line even if they do not +contain the old tag or did not exist on the specified date. +.SP +By default (without a \fB\-r\fP or \fB\-D\fP flag) +the versions to be tagged are supplied +implicitly by the \fBcvs\fP records of your working files' history +rather than applied explicitly. +.SP +If you use +.` "cvs tag \-d \fIsymbolic_tag\fP\|.\|.\|.", +the +symbolic tag you specify is +.I deleted +instead of being added. \fIWarning\fP: Be very certain of your ground +before you delete a tag; doing this effectively discards some +historical information, which may later turn out to have been valuable. +.SP +.` "cvs tag" +will not move a tag that already exists. With the \fB\-F\fP option, +however, +.` "cvs tag" +will re-locate any instance of \fIsymbolic_tag\fP that already exists +on that file to the new repository versions. Without the \fB\-F\fP +option, attempting to use +.` "cvs tag" +to apply a tag that already exists on that file will produce an error +message. +.SP +The \fB-b\fP option makes the tag a ``branch'' tag, allowing +concurrent, isolated development. +This is most useful for creating a patch to a previously released software +distribution. +.SP +Normally, +.B tag +executes recursively through subdirectories; you can prevent this by +using the standard \fB\-l\fP option, or specify the recursion +explicitly by using \fB\-R\fP. +.TP +\fBupdate\fP [\fB\-ACdf\|lPpQqR\fP] [\fB\-d\fP] [\fB\-r\fP \fItag\fP|\fB\-D\fP \fIdate\fP] \fIfiles\|.\|.\|.\fP +.I Requires: +repository, working directory. +.br +.I Changes: +working directory. +.br +After you've run +.B checkout +to create your private copy of source from the common repository, +other developers will continue changing the central source. From time +to time, when it is convenient in your development process, you can +use the +.B update +command +from within your working directory to reconcile your work with any +revisions applied to the source repository since your last +.B checkout +or +.BR update . +.SP +.B update +keeps you informed of its progress by printing a line for each file, +prefaced with one of the characters +.` "U A R M C ?" +to indicate the status of the file: +.TP 1i +\fBU\fP \fIfile\fP +The file was brought \fIup to date\fP with respect to the repository. +This is done for any file that exists in the repository but not in +your source, and for files that you haven't changed but are not the most +recent versions available in the repository. +.TP 1i +\fBA\fP \fIfile\fP +The file has been \fIadded\fP to your private copy of the sources, and +will be added to the +source repository when you run +.` "cvs commit" +on the file. +This is a reminder to you that the file needs to be committed. +.TP 1i +\fBR\fP \fIfile\fP +The file has been \fIremoved\fP from your private copy of the sources, and +will be removed from the +source repository when you run +.` "cvs commit" +on the file. +This is a reminder to you that the file needs to be committed. +.TP 1i +\fBM\fP \fIfile\fP +The file is \fImodified\fP in your working directory. +.` "M" +can indicate one of two states for a file you're working on: either +there were no modifications to the same file in the repository, so +that your file remains as you last saw it; or there were modifications +in the repository as well as in your copy, but they were +\fImerged\fP successfully, without conflict, in your working +directory. +.TP 1i +\fBC\fP \fIfile\fP +A \fIconflict\fP was detected while trying to merge your changes to +\fIfile\fP with changes from the source repository. \fIfile\fP (the +copy in your working directory) is now the result of merging +the two versions; an unmodified copy of your file is also +in your working directory, with the name `\fB.#\fP\fIfile\fP\fB.\fP\fIversion\fP', +where +.I version +is the +revision that your modified file started from. +(Note that some systems automatically purge files that begin with +\& +.` ".#" +if they have not been accessed for a few days. +If you intend to keep a copy of your original file, it is a very good +idea to rename it.) +.TP 1i +\fB?\fP \fIfile\fP +\fIfile\fP is in your working directory, but does not correspond to +anything in the source repository, and is not in the list of files +for \fBcvs\fP to ignore (see the description of the \fB\-I\fP option). +.PP +.RS .5i +.SP +Use the +.B \-A +option to reset any sticky tags, dates, or +.B \-k +options. (If you get a working copy of a file by using one of the +\fB\-r\fP, \fB\-D\fP, or \fB\-k\fP options, \fBcvs\fP remembers the +corresponding tag, date, or \fIkflag\fP and continues using it on +future updates; use the \fB\-A\fP option to make \fBcvs\fP forget these +specifications, and retrieve the ``head'' version of the file). +.SP +The \fB\-j\fP\fIbranch\fP option +merges the changes made between the +resulting revision and the revision that it is based on (e.g., if +the tag refers to a branch, +.B cvs +will merge all changes made in +that branch into your working file). +.SP +With two \fB-j\fP options, +.B cvs +will merge in the changes between the two respective revisions. +This can be used to ``remove'' a certain delta from your working file. +E.g., If the file foo.c is based on +revision 1.6 and I want to remove the changes made between 1.3 and +1.5, I might do: +.SP +.in +1i +.ft B +.nf +example% cvs update -j1.5 -j1.3 foo.c # note the order... +.fi +.ft P +.in -1i +.SP +In addition, each \fB-j\fP option can contain on optional date +specification which, when used with branches, can limit the chosen +revision to one within a specific date. +An optional date is specified by adding a colon (:) to the tag. +.SP +.in +1i +.ft B +.nf +-jSymbolic_Tag:Date_Specifier +.fi +.ft P +.in -1i +.SP +Use the +.B \-d +option to create any directories that exist in the repository if they're +missing from the working directory. (Normally, update acts only on +directories and files that were already enrolled in your +working directory.) This is useful for updating directories +that were created in the repository since the initial +\fBcheckout\fP; but it has an unfortunate side effect. If you +deliberately avoided certain directories in the repository when you +created your working directory (either through use of a module name or by +listing explicitly the files and directories you wanted on the +command line), then updating with +.B \-d +will create those directories, which may not be what you want. +.SP +Use \fB\-I\fP \fIname\fP to ignore files whose names match \fIname\fP +(in your working directory) during the update. You can specify +\fB\-I\fP more than once on the command line to specify several files +to ignore. By default, +\fBupdate\fP ignores files whose names match certain patterns; for +an up to date list of ignored file names, see the Cederqvist manual (as +described in the SEE ALSO section of this manpage). +.SP +Use +.` "\-I !" +to avoid ignoring any files at all. +.SP +Use the +.` "\-C" +option to overwrite locally modified files with clean copies from +the repository (the modified file is saved in +`\fB.#\fP\fIfile\fP\fB.\fP\fIrevision\fP', however). +.SP +The standard \fBcvs\fP command options \fB\-f\fP, \fB\-k\fP, +\fB\-l\fP, \fB\-P\fP, \fB\-p\fP, and \fB\-r\fP +are also available with \fBupdate\fP. +.RE +.SH "FILES" +For more detailed information on +.B cvs +supporting files, see +.BR cvs ( 5 ). +.LP +.I +Files in home directories: +.TP +\&.cvsrc +The +.B cvs +initialisation file. Lines in this file can be used to specify default +options for each +.B cvs +command. For example the line +.` "diff \-c" +will ensure that +.` "cvs diff" +is always passed the +.B \-c +option in addition to any other options passed on the command line. +.TP +\&.cvswrappers +Specifies wrappers to be used in addition to those specified in the +CVSROOT/cvswrappers file in the repository. +.LP +.I +Files in working directories: +.TP +CVS +A directory of \fBcvs\fP administrative files. +.I +Do not delete. +.TP +CVS/Entries +List and status of files in your working directory. +.TP +CVS/Entries.Backup +A backup of +.` "CVS/Entries". +.TP +CVS/Entries.Static +Flag: do not add more entries on +.` "cvs update". +.TP +CVS/Root +Pathname to the repository ( +.SM CVSROOT +) location at the time of checkout. This file is used instead +of the +.SM CVSROOT +environment variable if the environment variable is not +set. A warning message will be issued when the contents of this +file and the +.SM CVSROOT +environment variable differ. The file may be over-ridden by the +presence of the +.SM CVS_IGNORE_REMOTE_ROOT +environment variable. +.TP +CVS/Repository +Pathname to the corresponding directory in the source repository. +.TP +CVS/Tag +Contains the per-directory ``sticky'' tag or date information. +This file is created/updated when you specify +.B \-r +or +.B \-D +to the +.B checkout +or +.B update +commands, and no files are specified. +.TP +CVS/Checkin.prog +Name of program to run on +.` "cvs commit". +.TP +CVS/Update.prog +Name of program to run on +.` "cvs update". +.LP +.I +Files in source repositories: +.TP +$CVSROOT/CVSROOT +Directory of global administrative files for repository. +.TP +CVSROOT/commitinfo,v +Records programs for filtering +.` "cvs commit" +requests. +.TP +CVSROOT/cvswrappers,v +Records +.B cvs +wrapper commands to be used when checking files into and out of the +repository. Wrappers allow the file or directory to be processed +on the way in and out of CVS. The intended uses are many, one +possible use would be to reformat a C file before the file is checked +in, so all of the code in the repository looks the same. +.TP +CVSROOT/editinfo,v +Records programs for editing/validating +.` "cvs commit" +log entries. +.TP +CVSROOT/history +Log file of \fBcvs\fP transactions. +.TP +CVSROOT/loginfo,v +Records programs for piping +.` "cvs commit" +log entries. +.TP +CVSROOT/modules,v +Definitions for modules in this repository. +.TP +CVSROOT/rcsinfo,v +Records pathnames to templates used during a +.` "cvs commit" +operation. +.TP +CVSROOT/taginfo,v +Records programs for validating/logging +.` "cvs tag" +and +.` "cvs rtag" +operations. +.TP +MODULE/Attic +Directory for removed source files. +.TP +#cvs.lock +A lock directory created by +.B cvs +when doing sensitive changes to the +source repository. +.TP +#cvs.tfl.\fIpid\fP +Temporary lock file for repository. +.TP +#cvs.rfl.\fIpid\fP +A read lock. +.TP +#cvs.wfl.\fIpid\fP +A write lock. +.SH "ENVIRONMENT VARIABLES" +.TP +.SM CVSROOT +Should contain the full pathname to the root of the +.B cvs +source repository (where the +.SM RCS +files are kept). This information must be available to \fBcvs\fP for +most commands to execute; if +.SM CVSROOT +is not set, or if you wish to override it for one invocation, you can +supply it on the command line: +.` "cvs \-d \fIcvsroot cvs_command\fP\|.\|.\|." +You may not need to set +.SM CVSROOT +if your \fBcvs\fP binary has the right path compiled in; use +.` "cvs \-v" +to display all compiled-in paths. +.TP +.SM CVSREAD +If this is set, +.B checkout +and +.B update +will try hard to make the files in your working directory read-only. +When this is not set, the default behavior is to permit modification +of your working files. +.TP +.SM RCSBIN +Specifies the full pathname where to find +.SM RCS +programs, such as +.BR co ( 1 ) +and +.BR ci ( 1 ) +(CVS 1.9 and older). +.TP +.SM CVSEDITOR +Specifies the program to use for recording log messages during +.BR commit . +If not set, the +.SM EDITOR +environment variable is used instead. +If +.SM EDITOR +is not set either, the default is +.BR /usr/ucb/vi . +.TP +.SM CVS_IGNORE_REMOTE_ROOT +If this variable is set then +.B cvs +will ignore all references to remote repositories in the CVS/Root file. +.TP +.SM CVS_RSH +.B cvs +uses the contents of this variable to determine the name of the +remote shell command to use when starting a +.B cvs +server. If this variable is not set then +.` "rsh" +is used. +.TP +.SM CVS_SERVER +.B cvs +uses the contents of this variable to determine the name of the +.B cvs +server command. If this variable is not set then +.` "cvs" +is used. +.TP +.SM CVSWRAPPERS +This variable is used by the +.` "cvswrappers" +script to determine the name of the wrapper file, in addition to the +wrappers defaults contained in the repository +.SM (CVSROOT/cvswrappers) +and the user's home directory (~/.cvswrappers). +.SH "AUTHORS" +.TP +Dick Grune +Original author of the +.B cvs +shell script version posted to +.B comp.sources.unix +in the volume6 release of December, 1986. +Credited with much of the +.B cvs +conflict resolution algorithms. +.TP +Brian Berliner +Coder and designer of the +.B cvs +program itself in April, 1989, based on the original work done by Dick. +.TP +Jeff Polk +Helped Brian with the design of the +.B cvs +module and vendor branch support and author of the +.BR checkin ( 1 ) +shell script (the ancestor of +.` "cvs import"). +.TP +And many others too numerous to mention here. +.SH "SEE ALSO" +The most comprehensive manual for CVS is +Version Management with CVS by Per Cederqvist et al. Depending on +your system, you may be able to get it with the +.B info cvs +command or it may be available as cvs.ps (postscript), cvs.texinfo +(texinfo source), or cvs.html. +.SP +For CVS updates, more information on documentation, software related +to CVS, development of CVS, and more, see: +.in +1i +.B http://www.cyclic.com +.B http://www.loria.fr/~molli/cvs-index.html +.in -1i +.SP +.BR ci ( 1 ), +.BR co ( 1 ), +.BR cvs ( 5 ), +.BR cvsbug ( 8 ), +.BR diff ( 1 ), +.BR grep ( 1 ), +.BR patch ( 1 ), +.BR rcs ( 1 ), +.BR rcsdiff ( 1 ), +.BR rcsmerge ( 1 ), +.BR rlog ( 1 ). diff --git a/gnu/dist/cvs/man/cvs.5 b/gnu/dist/cvs/man/cvs.5 new file mode 100644 index 00000000000..fb2bcec0a09 --- /dev/null +++ b/gnu/dist/cvs/man/cvs.5 @@ -0,0 +1,325 @@ +.TH cvs 5 "12 February 1992" +.\" Full space in nroff; half space in troff +.de SP +.if n .sp +.if t .sp .5 +.. +.SH NAME +cvs \- Concurrent Versions System support files +.SH SYNOPSIS +.hy 0 +.na +.TP +.B $CVSROOT/CVSROOT/commitinfo,v +.TP +.B $CVSROOT/CVSROOT/cvsignore,v +.TP +.B $CVSROOT/CVSROOT/cvswrappers,v +.TP +.B $CVSROOT/CVSROOT/editinfo,v +.TP +.B $CVSROOT/CVSROOT/history +.TP +.B $CVSROOT/CVSROOT/loginfo,v +.TP +.B $CVSROOT/CVSROOT/modules,v +.TP +.B $CVSROOT/CVSROOT/rcsinfo,v +.TP +.B $CVSROOT/CVSROOT/taginfo,v +.ad b +.hy 1 +.SH DESCRIPTION +.B cvs +is a system for providing source control to hierarchical collections +of source directories. Commands and procedures for using \fBcvs\fP +are described in +.BR cvs ( 1 ). +.SP +.B cvs +manages \fIsource repositories\fP, the directories containing master +copies of the revision-controlled files, by copying particular +revisions of the files to (and modifications back from) developers' +private \fIworking directories\fP. In terms of file structure, each +individual source repository is an immediate subdirectory of +\fB$CVSROOT\fP. +.SP +The files described here are supporting files; they do not have to +exist for \fBcvs\fP to operate, but they allow you to make \fBcvs\fP +operation more flexible. +.SP +You can use the `\|modules\|' file to define symbolic names for +collections of source maintained with \fBcvs\fP. If there is no +`\|modules\|' file, developers must specify complete path names +(absolute, or relative to \fB$CVSROOT\fP) for the files they wish to +manage with \fBcvs\fP commands. +.SP +You can use the `\|commitinfo\|' file to define programs to execute +whenever `\|\fBcvs commit\fP\|' is about to execute. +These programs are used for ``pre-commit'' checking to verify that the +modified, added, and removed files are really ready to be committed. +Some uses for this check might be to turn off a portion (or all) of the +source repository from a particular person or group. +Or, perhaps, to verify that the changed files conform to the site's +standards for coding practice. +.SP +You can use the `\|cvswrappers\|' file to record +.B cvs +wrapper commands to be used when checking files into and out of the +repository. Wrappers allow the file or directory to be processed +on the way in and out of CVS. The intended uses are many, one +possible use would be to reformat a C file before the file is checked +in, so all of the code in the repository looks the same. +.SP +You can use the `\|loginfo\|' file to define programs to execute after +any +.BR commit , +which writes a log entry for changes in the repository. +These logging programs might be used to append the log message to a file. +Or send the log message through electronic mail to a group of developers. +Or, perhaps, post the log message to a particular newsgroup. +.SP +You can use the `\|taginfo\|' file to define programs to execute after +any +.BR tag or rtag +operation. These programs might be used to append a message to a file +listing the new tag name and the programmer who created it, or send mail +to a group of developers, or, perhaps, post a message to a particular +newsgroup. +.SP +You can use the `\|rcsinfo\|' file to define forms for log messages. +.SP +You can use the `\|editinfo\|' file to define a program to execute for +editing/validating `\|\fBcvs commit\fP\|' log entries. +This is most useful when used with a `\|rcsinfo\|' forms specification, as +it can verify that the proper fields of the form have been filled in by the +user committing the change. +.SP +You can use the `\|cvsignore\|' file to specify the default list of +files to ignore during \fBupdate\fP. +.SP +You can use the `\|history\|' file to record the \fBcvs\fP commands +that affect the repository. +The creation of this file enables history logging. +.SH FILES +.TP +.B modules +The `\|modules\|' file records your definitions of names for +collections of source code. \fBcvs\fP will use these definitions if +you use \fBcvs\fP to check in a file with the right format to +`\|\fB$CVSROOT/CVSROOT/modules,v\fP\|'. +.SP +The `\|modules\|' file may contain blank lines and comments (lines +beginning with `\|\fB#\fP\|') as well as module definitions. +Long lines can be continued on the next line by specifying a backslash +(``\e'') as the last character on the line. +.SP +A \fImodule definition\fP is a single line of the `\|modules\|' file, +in either of two formats. In both cases, \fImname\fP represents the +symbolic module name, and the remainder of the line is its definition. +.SP +\fImname\fP \fB\-a\fP \fIaliases\fP\|.\|.\|. +.br +This represents the simplest way of defining a module \fImname\fP. +The `\|\fB\-a\fP\|' flags the definition as a simple alias: \fBcvs\fP +will treat any use of \fImname\fP (as a command argument) as if the list +of names \fIaliases\fP had been specified instead. \fIaliases\fP may +contain either other module names or paths. When you use paths in +\fIaliases\fP, `\|\fBcvs checkout\fP\|' creates all intermediate +directories in the working directory, just as if the path had been +specified explicitly in the \fBcvs\fP arguments. +.SP +.nf +\fImname\fP [ \fIoptions\fP ] \fIdir\fP [ \fIfiles\fP\|.\|.\|. ] [ \fB&\fP\fImodule\fP\|.\|.\|. ] +.fi +.SP +In the simplest case, this form of module definition reduces to +`\|\fImname dir\fP\|'. This defines all the files in directory +\fIdir\fP as module \fImname\fP. \fIdir\fP is a relative path (from +\fB$CVSROOT\fP) to a directory of source in one of the source +repositories. In this case, on \fBcheckout\fP, a single directory +called \fImname\fP is created as a working directory; no intermediate +directory levels are used by default, even if \fIdir\fP was a path +involving several directory levels. +.SP +By explicitly specifying \fIfiles\fP in the module definition after +\fIdir\fP, you can select particular files from directory +\fIdir\fP. The sample definition for \fBmodules\fP is an example of +a module defined with a single file from a particular directory. Here +is another example: +.SP +.nf +.ft B +m4test unsupported/gnu/m4 foreach.m4 forloop.m4 +.ft P +.fi +.SP +With this definition, executing `\|\fBcvs checkout m4test\fP\|' +will create a single working directory `\|m4test\|' containing the two +files listed, which both come from a common directory several levels +deep in the \fBcvs\fP source repository. +.SP +A module definition can refer to other modules by including +`\|\fB&\fP\fImodule\fP\|' in its definition. \fBcheckout\fP creates +a subdirectory for each such \fImodule\fP, in your working directory. +.br +.I +New in \fBcvs\fP 1.3; +avoid this feature if sharing module definitions with older versions +of \fBcvs\fP. +.SP +Finally, you can use one or more of the following \fIoptions\fP in +module definitions: +.SP +\&`\|\fB\-d\fP \fIname\fP\|', to name the working directory something +other than the module name. +.br +.I +New in \fBcvs\fP 1.3; +avoid this feature if sharing module definitions with older versions +of \fBcvs\fP. +.SP +\&`\|\fB\-i\fP \fIprog\fP\|' allows you to specify a program \fIprog\fP +to run whenever files in a module are committed. \fIprog\fP runs with a +single argument, the full pathname of the affected directory in a +source repository. The `\|commitinfo\|', `\|loginfo\|', and +`\|editinfo\|' files provide other ways to call a program on \fBcommit\fP. +.SP +`\|\fB\-o\fP \fIprog\fP\|' allows you to specify a program \fIprog\fP +to run whenever files in a module are checked out. \fIprog\fP runs +with a single argument, the module name. +.SP +`\|\fB\-e\fP \fIprog\fP\|' allows you to specify a program \fIprog\fP +to run whenever files in a module are exported. \fIprog\fP runs +with a single argument, the module name. +.SP +`\|\fB\-t\fP \fIprog\fP\|' allows you to specify a program \fIprog\fP +to run whenever files in a module are tagged. \fIprog\fP runs with two +arguments: the module name and the symbolic tag specified to \fBrtag\fP. +.SP +`\|\fB\-u\fP \fIprog\fP\|' allows you to specify a program \fIprog\fP +to run whenever `\|\fBcvs update\fP\|' is executed from the top-level +directory of the checked-out module. \fIprog\fP runs with a +single argument, the full path to the source repository for this module. +.TP +\&\fBcommitinfo\fP, \fBloginfo\fP, \fBrcsinfo\fP, \fBeditinfo\fP +These files all specify programs to call at different points in the +`\|\fBcvs commit\fP\|' process. They have a common structure. +Each line is a pair of fields: a regular expression, separated by +whitespace from a filename or command-line template. +Whenever one of the regular expression matches a directory name in the +repository, the rest of the line is used. +If the line begins with a \fB#\fP character, the entire line is considered +a comment and is ignored. +Whitespace between the fields is also ignored. +.SP +For `\|loginfo\|', the rest of the +line is a command-line template to execute. +The templates can include not only +a program name, but whatever list of arguments you wish. If you write +`\|\fB%s\fP\|' somewhere on the argument list, \fBcvs\fP supplies, at +that point, the list of files affected by the \fBcommit\fP. +The first entry in the list is the relative path within the source +repository where the change is being made. +The remaining arguments list the files that are being modified, added, or +removed by this \fBcommit\fP invocation. +.SP +For `\|taginfo\|', the rest of the +line is a command-line template to execute. +The arguments passed to the command are, in order, the +.I tagname , +.I operation +(i.e. +.B add +for `tag', +.B mov +for `tag -F', and +.B del +for `tag -d`), +.I repository , +and any remaining are pairs of +.B "filename revision" . +A non-zero exit of the filter program will cause the tag to be aborted. +.SP +For `\|commitinfo\|', the rest of the line is a command-line template to +execute. +The template can include not only a program name, but whatever +list of arguments you wish. +The full path to the current source repository is appended to the template, +followed by the file names of any files involved in the commit (added, +removed, and modified files). +.SP +For `\|rcsinfo\|', the rest of the line is the full path to a file that +should be loaded into the log message template. +.SP +For `\|editinfo\|', the rest of the line is a command-line template to +execute. +The template can include not only a program name, but whatever +list of arguments you wish. +The full path to the current log message template file is appended to the +template. +.SP +You can use one of two special strings instead of a regular +expression: `\|\fBALL\fP\|' specifies a command line template that +must always be executed, and `\|\fBDEFAULT\fP\|' specifies a command +line template to use if no regular expression is a match. +.SP +The `\|commitinfo\|' file contains commands to execute \fIbefore\fP any +other \fBcommit\fP activity, to allow you to check any conditions that +must be satisfied before \fBcommit\fP can proceed. The rest of the +\fBcommit\fP will execute only if all selected commands from this file +exit with exit status \fB0\fP. +.SP +The `\|rcsinfo\|' file allows you to specify \fIlog templates\fP for +the \fBcommit\fP logging session; you can use this to provide a form +to edit when filling out the \fBcommit\fP log. The field after the +regular expression, in this file, contains filenames (of files +containing the logging forms) rather than command templates. +.SP +The `\|editinfo\|' file allows you to execute a script \fIbefore the +commit starts\fP, but after the log information is recorded. These +"edit" scripts can verify information recorded in the log file. If +the edit script exits wth a non-zero exit status, the commit is aborted. +.SP +The `\|loginfo\|' file contains commands to execute \fIat the end\fP +of a commit. The text specified as a commit log message is piped +through the command; typical uses include sending mail, filing an +article in a newsgroup, or appending to a central file. +.TP +\&\fBcvsignore\fP, \fB.cvsignore\fP +The default list of files (or +.BR sh ( 1 ) +file name patterns) to ignore during `\|\fBcvs update\fP\|'. +At startup time, \fBcvs\fP loads the compiled in default list of file name +patterns (see +.BR cvs ( 1 )). +Then the per-repository list included in \fB$CVSROOT/CVSROOT/cvsignore\fP +is loaded, if it exists. +Then the per-user list is loaded from `\|$HOME/.cvsignore\|'. +Finally, as \fBcvs\fP traverses through your directories, it will load any +per-directory `\|.cvsignore\|' files whenever it finds one. +These per-directory files are only valid for exactly the directory that +contains them, not for any sub-directories. +.TP +.B history +Create this file in \fB$CVSROOT/CVSROOT\fP to enable history logging +(see the description of `\|\fBcvs history\fP\|'). +.SH "SEE ALSO" +.BR cvs ( 1 ), +.SH COPYING +Copyright \(co 1992 Cygnus Support, Brian Berliner, and Jeff Polk +.PP +Permission is granted to make and distribute verbatim copies of +this manual provided the copyright notice and this permission notice +are preserved on all copies. +.PP +Permission is granted to copy and distribute modified versions of this +manual under the conditions for verbatim copying, provided that the +entire resulting derived work is distributed under the terms of a +permission notice identical to this one. +.PP +Permission is granted to copy and distribute translations of this +manual into another language, under the above conditions for modified +versions, except that this permission notice may be included in +translations approved by the Free Software Foundation instead of in +the original English. diff --git a/gnu/dist/cvs/man/cvsbug.8 b/gnu/dist/cvs/man/cvsbug.8 new file mode 100644 index 00000000000..ecf2eb8a7ee --- /dev/null +++ b/gnu/dist/cvs/man/cvsbug.8 @@ -0,0 +1,243 @@ +.\" -*- nroff -*- +.\" --------------------------------------------------------------------------- +.\" man page for send-pr (by Heinz G. Seidl, hgs@cygnus.com) +.\" updated Feb 1993 for GNATS 3.00 by Jeffrey Osier, jeffrey@cygnus.com +.\" +.\" This file is part of the Problem Report Management System (GNATS) +.\" Copyright 1992 Cygnus Support +.\" +.\" 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. +.\" +.\" --------------------------------------------------------------------------- +.nh +.TH CVSBUG 8 xVERSIONx "February 1993" +.SH NAME +cvsbug \- send problem report (PR) about CVS to a central support site +.SH SYNOPSIS +.B cvsbug +[ +.I site +] +[ +.B \-f +.I problem-report +] +[ +.B \-t +.I mail-address +] +.br +.in +0.8i +[ +.B \-P +] +[ +.B \-L +] +[ +.B \-\-request-id +] +[ +.B \-v +] +.SH DESCRIPTION +.B cvsbug +is a tool used to submit +.I problem reports +.\" SITE ADMINISTRATORS - change this if you use a local default +(PRs) to a central support site. In most cases the correct +.I site +will be the default. This argument indicates the support site which +is responsible for the category of problem involved. Some sites may +use a local address as a default. +.I site +values are defined by using the +.BR aliases (5). +.LP +.B cvsbug +invokes an editor on a problem report template (after trying to fill +in some fields with reasonable default values). When you exit the +editor, +.B cvsbug +sends the completed form to the +.I Problem Report Management System +(\fBGNATS\fR) at a central support site. At the support site, the PR +is assigned a unique number and is stored in the \fBGNATS\fR database +according to its category and submitter-id. \fBGNATS\fR automatically +replies with an acknowledgement, citing the category and the PR +number. +.LP +To ensure that a PR is handled promptly, it should contain your (unique) +\fIsubmitter-id\fR and one of the available \fIcategories\fR to identify the +problem area. (Use +.B `cvsbug -L' +to see a list of categories.) +.LP +The +.B cvsbug +template at your site should already be customized with your +submitter-id (running `\|\fBinstall-sid\fP \fIsubmitter-id\fP\|' to +accomplish this is part of the installation procedures for +.BR cvsbug ). +If this hasn't been done, see your system administrator for your +submitter-id, or request one from your support site by invoking +.B `cvsbug \-\-request\-id'. +If your site does not distinguish between different user sites, or if +you are not affiliated with the support site, use +.B `net' +for this field. +.LP +The more precise your problem description and the more complete your +information, the faster your support team can solve your problems. +.SH OPTIONS +.TP +.BI \-f " problem-report" +specify a file (\fIproblem-report\fR) which already contains a +complete problem report. +.B cvsbug +sends the contents of the file without invoking the editor. If +the value for +.I problem-report +is +.BR `\|\-\|' , +then +.B cvsbug +reads from standard input. +.TP +.BI \-t " mail-address" +Change mail address at the support site for problem reports. The +default +.I mail-address +is the address used for the default +.IR site . +Use the +.I site +argument rather than this option in nearly all cases. +.TP +.B \-P +print the form specified by the environment variable +.B PR_FORM +on standard output. If +.B PR_FORM +is not set, print the standard blank PR template. No mail is sent. +.TP +.B -L +print the list of available categories. No mail is sent. +.TP +.B \-\-request\-id +sends mail to the default support site, or +.I site +if specified, with a request for your +.IR submitter-id . +If you are +not affiliated with +.IR site , +use a +.I submitter-id +of +.BR net \|'. +.TP +.B \-v +Display the +.B cvsbug +version number. +.LP +Note: use +.B cvsbug +to submit problem reports rather than mailing them directly. Using +both the template and +.B cvsbug +itself will help ensure all necessary information will reach the +support site. +.SH ENVIRONMENT +The environment variable +.B EDITOR +specifies the editor to invoke on the template. +.br +default: +.B vi +.sp +If the environment variable +.B PR_FORM +is set, then its value is used as the file name of the template for +your problem-report editing session. You can use this to start with a +partially completed form (for example, a form with the identification +fields already completed). +.SH "HOW TO FILL OUT A PROBLEM REPORT" +Problem reports have to be in a particular form so that a program can +easily manage them. Please remember the following guidelines: +.IP \(bu 3m +describe only +.B one problem +with each problem report. +.IP \(bu 3m +For follow-up mail, use the same subject line as the one in the automatic +acknowledgent. It consists of category, PR number and the original synopsis +line. This allows the support site to relate several mail messages to a +particular PR and to record them automatically. +.IP \(bu 3m +Please try to be as accurate as possible in the subject and/or synopsis line. +.IP \(bu 3m +The subject and the synopsis line are not confidential. This is +because open-bugs lists are compiled from them. Avoid confidential +information there. +.LP +See the GNU +.B Info +file +.B cvsbug.info +or the document \fIReporting Problems With cvsbug\fR\ for detailed +information on reporting problems +.SH "HOW TO SUBMIT TEST CASES, CODE, ETC." +Submit small code samples with the PR. Contact the support site for +instructions on submitting larger test cases and problematic source +code. +.SH FILES +.ta \w'/tmp/pbad$$ 'u +/tmp/p$$ copy of PR used in editing session +.br +/tmp/pf$$ copy of empty PR form, for testing purposes +.br +/tmp/pbad$$ file for rejected PRs +.SH INSTALLATION AND CONFIGURATION +See +.B INSTALL +for installation instructions. +.SH SEE ALSO +.BR gnats (l), +.BR query-pr (1), +.BR edit-pr (1), +.BR gnats (8), +.BR queue-pr (8), +.BR at-pr (8), +.BR mkcat (8), +.BR mkdist (8). +.SH AUTHORS +Jeffrey Osier, Brendan Kehoe, Jason Merrill, Heinz G. Seidl (Cygnus +Support) +.SH COPYING +Copyright (c) 1992, 1993 Free Software Foundation, Inc. +.PP +Permission is granted to make and distribute verbatim copies of +this manual provided the copyright notice and this permission notice +are preserved on all copies. +.PP +Permission is granted to copy and distribute modified versions of this +manual under the conditions for verbatim copying, provided that the +entire resulting derived work is distributed under the terms of a +permission notice identical to this one. +.PP +Permission is granted to copy and distribute translations of this +manual into another language, under the above conditions for modified +versions, except that this permission notice may be included in +translations approved by the Free Software Foundation instead of in +the original English. + diff --git a/gnu/dist/cvs/src/build_src.com b/gnu/dist/cvs/src/build_src.com new file mode 100755 index 00000000000..3d3baa65891 --- /dev/null +++ b/gnu/dist/cvs/src/build_src.com @@ -0,0 +1,68 @@ +$ CC :== CC/NOOPT/DEB/STANDARD=VAXC/DEFINE=HAVE_CONFIG_H- +/INCLUDE_DIR=([-],[-.VMS],[-.LIB])/PREFIX=ALL +$ CC add.c +$ CC admin.c +$ CC buffer.c +$ CC checkin.c +$ CC checkout.c +$ CC classify.c +$ CC client.c +$ CC commit.c +$ CC create_adm.c +$ CC cvsrc.c +$ CC diff.c +$ CC edit.c +$ CC entries.c +$ CC error.c +$ CC expand_path.c +$ CC fileattr.c +$ CC find_names.c +$ CC hash.c +$ CC history.c +$ CC ignore.c +$ CC import.c +$ CC lock.c +$ CC log.c +$ CC login.c +$ CC logmsg.c +$ CC main.c +$ CC mkmodules.c +$ CC modules.c +$ CC myndbm.c +$ CC no_diff.c +$ CC parseinfo.c +$ CC patch.c +$ CC rcs.c +$ CC/INCLUDE_DIR=([-],[-.VMS],[-.LIB],[-.diff]) rcscmds.c +$ CC recurse.c +$ CC release.c +$ CC remove.c +$ CC repos.c +$ CC root.c +$ CC rtag.c +$ CC run.c +$ CC scramble.c +$ CC server.c +$ CC status.c +$ CC subr.c +$ CC tag.c +$ CC update.c +$ CC version.c +$ CC vers_ts.c +$ CC watch.c +$ CC wrapper.c +$ CC/INCLUDE_DIR=([-],[-.VMS],[-.LIB],[-.zlib]) zlib.c +$ LIBRARY/CREATE cvslib.olb add.obj,admin.obj,buffer.obj,checkin.obj,- +checkout.obj,- +classify.obj,client.obj,commit.obj,create_adm.obj,cvsrc.obj,- +diff.obj,edit.obj,entries.obj,error.obj,expand_path.obj,fileattr.obj,- +find_names.obj,hash.obj,history.obj,ignore.obj,import.obj,- +lock.obj,log.obj,login.obj,logmsg.obj,mkmodules.obj,modules.obj,myndbm.obj,- +no_diff.obj,- +parseinfo.obj,patch.obj,rcs.obj,rcscmds.obj,recurse.obj,release.obj,- +remove.obj,repos.obj,root.obj,rtag.obj,run.obj,scramble.obj,server.obj,- +status.obj,- +subr.obj,tag.obj,update.obj,version.obj,vers_ts.obj,watch.obj,wrapper.obj,- +zlib.obj +$ link/nodeb/exe=cvs.exe main.obj,cvslib.olb/lib,[-.lib]gnulib.olb/lib,- +[-.vms]openvmslib.olb/lib,[-.zlib]zlib.olb/lib,[-.diff]diff.olb/lib diff --git a/gnu/dist/cvs/src/diff.c b/gnu/dist/cvs/src/diff.c new file mode 100644 index 00000000000..e98938d9b55 --- /dev/null +++ b/gnu/dist/cvs/src/diff.c @@ -0,0 +1,996 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Difference + * + * Run diff against versions in the repository. Options that are specified are + * passed on directly to "rcsdiff". + * + * Without any file arguments, runs diff against all the currently modified + * files. + */ + +#include "cvs.h" + +enum diff_file +{ + DIFF_ERROR, + DIFF_ADDED, + DIFF_REMOVED, + DIFF_DIFFERENT, + DIFF_SAME +}; + +static Dtype diff_dirproc PROTO ((void *callerdat, char *dir, + char *pos_repos, char *update_dir, + List *entries)); +static int diff_filesdoneproc PROTO ((void *callerdat, int err, + char *repos, char *update_dir, + List *entries)); +static int diff_dirleaveproc PROTO ((void *callerdat, char *dir, + int err, char *update_dir, + List *entries)); +static enum diff_file diff_file_nodiff PROTO ((struct file_info *finfo, + Vers_TS *vers, + enum diff_file)); +static int diff_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static void diff_mark_errors PROTO((int err)); + + +/* Global variables. Would be cleaner if we just put this stuff in a + struct like log.c does. */ + +/* Command line tags, from -r option. Points into argv. */ +static char *diff_rev1, *diff_rev2; +/* Command line dates, from -D option. Malloc'd. */ +static char *diff_date1, *diff_date2; +static char *use_rev1, *use_rev2; +static int have_rev1_label, have_rev2_label; + +/* Revision of the user file, if it is unchanged from something in the + repository and we want to use that fact. */ +static char *user_file_rev; + +static char *options; +static char *opts; +static size_t opts_allocated = 1; +static int diff_errors; +static int empty_files = 0; + +/* FIXME: should be documenting all the options here. They don't + perfectly match rcsdiff options (for example, we always support + --ifdef and --context, but rcsdiff only does if diff does). */ +static const char *const diff_usage[] = +{ + "Usage: %s %s [-lNR] [rcsdiff-options]\n", + " [[-r rev1 | -D date1] [-r rev2 | -D date2]] [files...] \n", + "\t-l\tLocal directory only, not recursive\n", + "\t-R\tProcess directories recursively.\n", + "\t-D d1\tDiff revision for date against working file.\n", + "\t-D d2\tDiff rev1/date1 against date2.\n", + "\t-N\tinclude diffs for added and removed files.\n", + "\t-r rev1\tDiff revision for rev1 against working file.\n", + "\t-r rev2\tDiff rev1/date1 against rev2.\n", + "\t--ifdef=arg\tOutput diffs in ifdef format.\n", + "(consult the documentation for your diff program for rcsdiff-options.\n", + "The most popular is -c for context diffs but there are many more).\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* I copied this array directly out of diff.c in diffutils 2.7, after + removing the following entries, none of which seem relevant to use + with CVS: + --help + --version + --recursive + --unidirectional-new-file + --starting-file + --exclude + --exclude-from + --sdiff-merge-assist + + I changed the options which take optional arguments (--context and + --unified) to return a number rather than a letter, so that the + optional argument could be handled more easily. I changed the + --paginate and --brief options to return a number, since -l and -q + mean something else to cvs diff. + + The numbers 129- that appear in the fourth element of some entries + tell the big switch in `diff' how to process those options. -- Ian + + The following options, which diff lists as "An alias, no longer + recommended" have been removed: --file-label --entire-new-file + --ascii --print. */ + +static struct option const longopts[] = +{ + {"ignore-blank-lines", 0, 0, 'B'}, + {"context", 2, 0, 143}, + {"ifdef", 1, 0, 131}, + {"show-function-line", 1, 0, 'F'}, + {"speed-large-files", 0, 0, 'H'}, + {"ignore-matching-lines", 1, 0, 'I'}, + {"label", 1, 0, 'L'}, + {"new-file", 0, 0, 'N'}, + {"initial-tab", 0, 0, 148}, + {"width", 1, 0, 'W'}, + {"text", 0, 0, 'a'}, + {"ignore-space-change", 0, 0, 'b'}, + {"minimal", 0, 0, 'd'}, + {"ed", 0, 0, 'e'}, + {"forward-ed", 0, 0, 'f'}, + {"ignore-case", 0, 0, 'i'}, + {"paginate", 0, 0, 144}, + {"rcs", 0, 0, 'n'}, + {"show-c-function", 0, 0, 'p'}, + + /* This is a potentially very useful option, except the output is so + silly. It would be much better for it to look like "cvs rdiff -s" + which displays all the same info, minus quite a few lines of + extraneous garbage. */ + {"brief", 0, 0, 145}, + + {"report-identical-files", 0, 0, 's'}, + {"expand-tabs", 0, 0, 't'}, + {"ignore-all-space", 0, 0, 'w'}, + {"side-by-side", 0, 0, 147}, + {"unified", 2, 0, 146}, + {"left-column", 0, 0, 129}, + {"suppress-common-lines", 0, 0, 130}, + {"old-line-format", 1, 0, 132}, + {"new-line-format", 1, 0, 133}, + {"unchanged-line-format", 1, 0, 134}, + {"line-format", 1, 0, 135}, + {"old-group-format", 1, 0, 136}, + {"new-group-format", 1, 0, 137}, + {"unchanged-group-format", 1, 0, 138}, + {"changed-group-format", 1, 0, 139}, + {"horizon-lines", 1, 0, 140}, + {"binary", 0, 0, 142}, + {0, 0, 0, 0} +}; + +/* CVS 1.9 and similar versions seemed to have pretty weird handling + of -y and -T. In the cases where it called rcsdiff, + they would have the meanings mentioned below. In the cases where it + called diff, they would have the meanings mentioned in "longopts". + Noone seems to have missed them, so I think the right thing to do is + just to remove the options altogether (which I have done). + + In the case of -z and -q, "cvs diff" did not accept them even back + when we called rcsdiff (at least, it hasn't accepted them + recently). + + In comparing rcsdiff to the new CVS implementation, I noticed that + the following rcsdiff flags are not handled by CVS diff: + + -y: perform diff even when the requested revisions are the + same revision number + -q: run quietly + -T: preserve modification time on the RCS file + -z: specify timezone for use in file labels + + I think these are not really relevant. -y is undocumented even in + RCS 5.7, and seems like a minor change at best. According to RCS + documentation, -T only applies when a RCS file has been modified + because of lock changes; doesn't CVS sidestep RCS's entire lock + structure? -z seems to be unsupported by CVS diff, and has a + different meaning as a global option anyway. (Adding it could be + a feature, but if it is left out for now, it should not break + anything.) For the purposes of producing output, CVS diff appears + mostly to ignore -q. Maybe this should be fixed, but I think it's + a larger issue than the changes included here. */ + +static void strcat_and_allocate PROTO ((char **, size_t *, const char *)); + +/* *STR is a pointer to a malloc'd string. *LENP is its allocated + length. Add SRC to the end of it, reallocating if necessary. */ +static void +strcat_and_allocate (str, lenp, src) + char **str; + size_t *lenp; + const char *src; +{ + size_t new_size; + + new_size = strlen (*str) + strlen (src) + 1; + if (*str == NULL || new_size >= *lenp) + { + while (new_size >= *lenp) + *lenp *= 2; + *str = xrealloc (*str, *lenp); + } + strcat (*str, src); +} + +int +diff (argc, argv) + int argc; + char **argv; +{ + char tmp[50]; + int c, err = 0; + int local = 0; + int which; + int option_index; + + if (argc == -1) + usage (diff_usage); + + have_rev1_label = have_rev2_label = 0; + + /* + * Note that we catch all the valid arguments here, so that we can + * intercept the -r arguments for doing revision diffs; and -l/-R for a + * non-recursive/recursive diff. + */ + + /* Clean out our global variables (multiroot can call us multiple + times and the server can too, if the client sends several + diff commands). */ + if (opts == NULL) + { + opts_allocated = 1; + opts = xmalloc (opts_allocated); + } + opts[0] = '\0'; + diff_rev1 = NULL; + diff_rev2 = NULL; + diff_date1 = NULL; + diff_date2 = NULL; + + optind = 0; + while ((c = getopt_long (argc, argv, + "+abcdefhilnpstuw0123456789BHNRC:D:F:I:L:U:V:W:k:r:", + longopts, &option_index)) != -1) + { + switch (c) + { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case 'h': case 'i': case 'n': case 'p': case 's': case 't': + case 'u': case 'w': case '0': case '1': case '2': + case '3': case '4': case '5': case '6': case '7': case '8': + case '9': case 'B': case 'H': + (void) sprintf (tmp, " -%c", (char) c); + strcat_and_allocate (&opts, &opts_allocated, tmp); + break; + case 'L': + if (have_rev1_label++) + if (have_rev2_label++) + { + error (0, 0, "extra -L arguments ignored"); + break; + } + + strcat_and_allocate (&opts, &opts_allocated, " -L"); + strcat_and_allocate (&opts, &opts_allocated, optarg); + break; + case 'C': case 'F': case 'I': case 'U': case 'V': case 'W': + (void) sprintf (tmp, " -%c", (char) c); + strcat_and_allocate (&opts, &opts_allocated, tmp); + strcat_and_allocate (&opts, &opts_allocated, optarg); + break; + case 131: + /* --ifdef. */ + strcat_and_allocate (&opts, &opts_allocated, " --ifdef="); + strcat_and_allocate (&opts, &opts_allocated, optarg); + break; + case 129: case 130: case 132: case 133: case 134: + case 135: case 136: case 137: case 138: case 139: case 140: + case 141: case 142: case 143: case 144: case 145: case 146: + case 147: case 148: + strcat_and_allocate (&opts, &opts_allocated, " --"); + strcat_and_allocate (&opts, &opts_allocated, + longopts[option_index].name); + if (longopts[option_index].has_arg == 1 + || (longopts[option_index].has_arg == 2 + && optarg != NULL)) + { + strcat_and_allocate (&opts, &opts_allocated, "="); + strcat_and_allocate (&opts, &opts_allocated, optarg); + } + break; + case 'R': + local = 0; + break; + case 'l': + local = 1; + break; + case 'k': + if (options) + free (options); + options = RCS_check_kflag (optarg); + break; + case 'r': + if (diff_rev2 != NULL || diff_date2 != NULL) + error (1, 0, + "no more than two revisions/dates can be specified"); + if (diff_rev1 != NULL || diff_date1 != NULL) + diff_rev2 = optarg; + else + diff_rev1 = optarg; + break; + case 'D': + if (diff_rev2 != NULL || diff_date2 != NULL) + error (1, 0, + "no more than two revisions/dates can be specified"); + if (diff_rev1 != NULL || diff_date1 != NULL) + diff_date2 = Make_Date (optarg); + else + diff_date1 = Make_Date (optarg); + break; + case 'N': + empty_files = 1; + break; + case '?': + default: + usage (diff_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* make sure options is non-null */ + if (!options) + options = xstrdup (""); + +#ifdef CLIENT_SUPPORT + if (client_active) { + /* We're the client side. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (local) + send_arg("-l"); + if (empty_files) + send_arg("-N"); + send_option_string (opts); + if (options[0] != '\0') + send_arg (options); + if (diff_rev1) + option_with_arg ("-r", diff_rev1); + if (diff_date1) + client_senddate (diff_date1); + if (diff_rev2) + option_with_arg ("-r", diff_rev2); + if (diff_date2) + client_senddate (diff_date2); + + /* Send the current files unless diffing two revs from the archive */ + if (diff_rev2 == NULL && diff_date2 == NULL) + send_files (argc, argv, local, 0, 0); + else + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + + send_file_names (argc, argv, SEND_EXPAND_WILD); + + send_to_server ("diff\012", 0); + err = get_responses_and_close (); + free (options); + options = NULL; + return (err); + } +#endif + + if (diff_rev1 != NULL) + tag_check_valid (diff_rev1, argc, argv, local, 0, ""); + if (diff_rev2 != NULL) + tag_check_valid (diff_rev2, argc, argv, local, 0, ""); + + which = W_LOCAL; + if (diff_rev1 != NULL || diff_date1 != NULL) + which |= W_REPOS | W_ATTIC; + + wrap_setup (); + + /* start the recursion processor */ + err = start_recursion (diff_fileproc, diff_filesdoneproc, diff_dirproc, + diff_dirleaveproc, NULL, argc, argv, local, + which, 0, 1, (char *) NULL, 1); + + /* clean up */ + free (options); + options = NULL; + + if (diff_date1 != NULL) + free (diff_date1); + if (diff_date2 != NULL) + free (diff_date2); + + return (err); +} + +/* + * Do a file diff + */ +/* ARGSUSED */ +static int +diff_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + int status, err = 2; /* 2 == trouble, like rcsdiff */ + Vers_TS *vers; + enum diff_file empty_file = DIFF_DIFFERENT; + char *tmp; + char *tocvsPath; + char *fname; + + /* Initialize these solely to avoid warnings from gcc -Wall about + variables that might be used uninitialized. */ + tmp = NULL; + fname = NULL; + + user_file_rev = 0; + vers = Version_TS (finfo, NULL, NULL, NULL, 1, 0); + + if (diff_rev2 != NULL || diff_date2 != NULL) + { + /* Skip all the following checks regarding the user file; we're + not using it. */ + } + else if (vers->vn_user == NULL) + { + /* The file does not exist in the working directory. */ + if ((diff_rev1 != NULL || diff_date1 != NULL) + && vers->srcfile != NULL) + { + /* The file does exist in the repository. */ + if (empty_files) + empty_file = DIFF_REMOVED; + else + { + int exists; + + exists = 0; + /* special handling for TAG_HEAD */ + if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0) + { + char *head = + (vers->vn_rcs == NULL + ? NULL + : RCS_branch_head (vers->srcfile, vers->vn_rcs)); + exists = head != NULL; + if (head != NULL) + free (head); + } + else + { + Vers_TS *xvers; + + xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, + 1, 0); + exists = xvers->vn_rcs != NULL; + freevers_ts (&xvers); + } + if (exists) + error (0, 0, + "%s no longer exists, no comparison available", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + else + { + error (0, 0, "I know nothing about %s", finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0') + { + if (empty_files) + empty_file = DIFF_ADDED; + else + { + error (0, 0, "%s is a new entry, no comparison available", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + else if (vers->vn_user[0] == '-') + { + if (empty_files) + empty_file = DIFF_REMOVED; + else + { + error (0, 0, "%s was removed, no comparison available", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + else + { + if (vers->vn_rcs == NULL && vers->srcfile == NULL) + { + error (0, 0, "cannot find revision control file for %s", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + else + { + if (vers->ts_user == NULL) + { + error (0, 0, "cannot find %s", finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + else if (!strcmp (vers->ts_user, vers->ts_rcs)) + { + /* The user file matches some revision in the repository + Diff against the repository (for remote CVS, we might not + have a copy of the user file around). */ + user_file_rev = vers->vn_user; + } + } + } + + empty_file = diff_file_nodiff (finfo, vers, empty_file); + if (empty_file == DIFF_SAME || empty_file == DIFF_ERROR) + { + freevers_ts (&vers); + if (empty_file == DIFF_SAME) + { + /* In the server case, would be nice to send a "Checked-in" + response, so that the client can rewrite its timestamp. + server_checked_in by itself isn't the right thing (it + needs a server_register), but I'm not sure what is. + It isn't clear to me how "cvs status" handles this (that + is, for a client which sends Modified not Is-modified to + "cvs status"), but it does. */ + return (0); + } + else + { + diff_mark_errors (err); + return (err); + } + } + + if (empty_file == DIFF_DIFFERENT) + { + int dead1, dead2; + + if (use_rev1 == NULL) + dead1 = 0; + else + dead1 = RCS_isdead (vers->srcfile, use_rev1); + if (use_rev2 == NULL) + dead2 = 0; + else + dead2 = RCS_isdead (vers->srcfile, use_rev2); + + if (dead1 && dead2) + { + freevers_ts (&vers); + return (0); + } + else if (dead1) + { + if (empty_files) + empty_file = DIFF_ADDED; + else + { + error (0, 0, "%s is a new entry, no comparison available", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + else if (dead2) + { + if (empty_files) + empty_file = DIFF_REMOVED; + else + { + error (0, 0, "%s was removed, no comparison available", + finfo->fullname); + freevers_ts (&vers); + diff_mark_errors (err); + return (err); + } + } + } + + /* Output an "Index:" line for patch to use */ + cvs_output ("Index: ", 0); + cvs_output (finfo->fullname, 0); + cvs_output ("\n", 1); + + tocvsPath = wrap_tocvs_process_file(finfo->file); + if (tocvsPath) + { + /* Backup the current version of the file to CVS/,,filename */ + fname = xmalloc (strlen (finfo->file) + + sizeof CVSADM + + sizeof CVSPREFIX + + 10); + sprintf(fname,"%s/%s%s",CVSADM, CVSPREFIX, finfo->file); + if (unlink_file_dir (fname) < 0) + if (! existence_error (errno)) + error (1, errno, "cannot remove %s", fname); + rename_file (finfo->file, fname); + /* Copy the wrapped file to the current directory then go to work */ + copy_file (tocvsPath, finfo->file); + } + + if (empty_file == DIFF_ADDED || empty_file == DIFF_REMOVED) + { + /* This is file, not fullname, because it is the "Index:" line which + is supposed to contain the directory. */ + cvs_output ("\ +===================================================================\n\ +RCS file: ", 0); + cvs_output (finfo->file, 0); + cvs_output ("\n", 1); + + cvs_output ("diff -N ", 0); + cvs_output (finfo->file, 0); + cvs_output ("\n", 1); + + if (empty_file == DIFF_ADDED) + { + if (use_rev2 == NULL) + status = diff_exec (DEVNULL, finfo->file, opts, RUN_TTY); + else + { + int retcode; + + tmp = cvs_temp_name (); + retcode = RCS_checkout (vers->srcfile, (char *) NULL, + use_rev2, (char *) NULL, + (*options + ? options + : vers->options), + tmp, (RCSCHECKOUTPROC) NULL, + (void *) NULL); + if (retcode != 0) + { + diff_mark_errors (err); + return err; + } + + status = diff_exec (DEVNULL, tmp, opts, RUN_TTY); + } + } + else + { + int retcode; + + tmp = cvs_temp_name (); + retcode = RCS_checkout (vers->srcfile, (char *) NULL, + use_rev1, (char *) NULL, + *options ? options : vers->options, + tmp, (RCSCHECKOUTPROC) NULL, + (void *) NULL); + if (retcode != 0) + { + diff_mark_errors (err); + return err; + } + + status = diff_exec (tmp, DEVNULL, opts, RUN_TTY); + } + } + else + { + char *label1 = NULL; + char *label2 = NULL; + + if (!have_rev1_label) + label1 = + make_file_label (finfo->fullname, use_rev1, vers->srcfile); + + if (!have_rev2_label) + label2 = + make_file_label (finfo->fullname, use_rev2, vers->srcfile); + + status = RCS_exec_rcsdiff (vers->srcfile, opts, + *options ? options : vers->options, + use_rev1, use_rev2, + label1, label2, + finfo->file); + + if (label1) free (label1); + if (label2) free (label2); + } + + switch (status) + { + case -1: /* fork failed */ + error (1, errno, "fork failed while diffing %s", + vers->srcfile->path); + case 0: /* everything ok */ + err = 0; + break; + default: /* other error */ + err = status; + break; + } + + if (tocvsPath) + { + if (unlink_file_dir (finfo->file) < 0) + if (! existence_error (errno)) + error (1, errno, "cannot remove %s", finfo->file); + + rename_file (fname, finfo->file); + if (unlink_file (tocvsPath) < 0) + error (1, errno, "cannot remove %s", tocvsPath); + free (fname); + } + + if (empty_file == DIFF_REMOVED + || (empty_file == DIFF_ADDED && use_rev2 != NULL)) + { + if (CVS_UNLINK (tmp) < 0) + error (0, errno, "cannot remove %s", tmp); + free (tmp); + } + + freevers_ts (&vers); + diff_mark_errors (err); + return (err); +} + +/* + * Remember the exit status for each file. + */ +static void +diff_mark_errors (err) + int err; +{ + if (err > diff_errors) + diff_errors = err; +} + +/* + * Print a warm fuzzy message when we enter a dir + * + * Don't try to diff directories that don't exist! -- DW + */ +/* ARGSUSED */ +static Dtype +diff_dirproc (callerdat, dir, pos_repos, update_dir, entries) + void *callerdat; + char *dir; + char *pos_repos; + char *update_dir; + List *entries; +{ + /* XXX - check for dirs we don't want to process??? */ + + /* YES ... for instance dirs that don't exist!!! -- DW */ + if (!isdir (dir)) + return (R_SKIP_ALL); + + if (!quiet) + error (0, 0, "Diffing %s", update_dir); + return (R_PROCESS); +} + +/* + * Concoct the proper exit status - done with files + */ +/* ARGSUSED */ +static int +diff_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + char *repos; + char *update_dir; + List *entries; +{ + return (diff_errors); +} + +/* + * Concoct the proper exit status - leaving directories + */ +/* ARGSUSED */ +static int +diff_dirleaveproc (callerdat, dir, err, update_dir, entries) + void *callerdat; + char *dir; + int err; + char *update_dir; + List *entries; +{ + return (diff_errors); +} + +/* + * verify that a file is different + */ +static enum diff_file +diff_file_nodiff (finfo, vers, empty_file) + struct file_info *finfo; + Vers_TS *vers; + enum diff_file empty_file; +{ + Vers_TS *xvers; + int retcode; + + /* free up any old use_rev* variables and reset 'em */ + if (use_rev1) + free (use_rev1); + if (use_rev2) + free (use_rev2); + use_rev1 = use_rev2 = (char *) NULL; + + if (diff_rev1 || diff_date1) + { + /* special handling for TAG_HEAD */ + if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0) + use_rev1 = ((vers->vn_rcs == NULL || vers->srcfile == NULL) + ? NULL + : RCS_branch_head (vers->srcfile, vers->vn_rcs)); + else + { + xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, 1, 0); + if (xvers->vn_rcs != NULL) + use_rev1 = xstrdup (xvers->vn_rcs); + freevers_ts (&xvers); + } + } + if (diff_rev2 || diff_date2) + { + /* special handling for TAG_HEAD */ + if (diff_rev2 && strcmp (diff_rev2, TAG_HEAD) == 0) + use_rev2 = ((vers->vn_rcs == NULL || vers->srcfile == NULL) + ? NULL + : RCS_branch_head (vers->srcfile, vers->vn_rcs)); + else + { + xvers = Version_TS (finfo, NULL, diff_rev2, diff_date2, 1, 0); + if (xvers->vn_rcs != NULL) + use_rev2 = xstrdup (xvers->vn_rcs); + freevers_ts (&xvers); + } + + if (use_rev1 == NULL) + { + /* The first revision does not exist. If EMPTY_FILES is + true, treat this as an added file. Otherwise, warn + about the missing tag. */ + if (use_rev2 == NULL) + /* At least in the case where DIFF_REV1 and DIFF_REV2 + are both numeric, we should be returning some kind + of error (see basicb-8a0 in testsuite). The symbolic + case may be more complicated. */ + return DIFF_SAME; + else if (empty_files) + return DIFF_ADDED; + else if (diff_rev1) + error (0, 0, "tag %s is not in file %s", diff_rev1, + finfo->fullname); + else + error (0, 0, "no revision for date %s in file %s", + diff_date1, finfo->fullname); + return DIFF_ERROR; + } + + if (use_rev2 == NULL) + { + /* The second revision does not exist. If EMPTY_FILES is + true, treat this as a removed file. Otherwise warn + about the missing tag. */ + if (empty_files) + return DIFF_REMOVED; + else if (diff_rev2) + error (0, 0, "tag %s is not in file %s", diff_rev2, + finfo->fullname); + else + error (0, 0, "no revision for date %s in file %s", + diff_date2, finfo->fullname); + return DIFF_ERROR; + } + + /* now, see if we really need to do the diff */ + if (strcmp (use_rev1, use_rev2) == 0) + return DIFF_SAME; + else + return DIFF_DIFFERENT; + } + + if ((diff_rev1 || diff_date1) && use_rev1 == NULL) + { + /* The first revision does not exist, and no second revision + was given. */ + if (empty_files) + { + if (empty_file == DIFF_REMOVED) + return DIFF_SAME; + else + { + if (user_file_rev && use_rev2 == NULL) + use_rev2 = xstrdup (user_file_rev); + return DIFF_ADDED; + } + } + else + { + if (diff_rev1) + error (0, 0, "tag %s is not in file %s", diff_rev1, + finfo->fullname); + else + error (0, 0, "no revision for date %s in file %s", + diff_date1, finfo->fullname); + return DIFF_ERROR; + } + } + + if (user_file_rev) + { + /* drop user_file_rev into first unused use_rev */ + if (!use_rev1) + use_rev1 = xstrdup (user_file_rev); + else if (!use_rev2) + use_rev2 = xstrdup (user_file_rev); + /* and if not, it wasn't needed anyhow */ + user_file_rev = 0; + } + + /* now, see if we really need to do the diff */ + if (use_rev1 && use_rev2) + { + if (strcmp (use_rev1, use_rev2) == 0) + return DIFF_SAME; + else + return DIFF_DIFFERENT; + } + + if (use_rev1 == NULL + || (vers->vn_user != NULL && strcmp (use_rev1, vers->vn_user) == 0)) + { + if (empty_file == DIFF_DIFFERENT + && vers->ts_user != NULL + && strcmp (vers->ts_rcs, vers->ts_user) == 0 + && (!(*options) || strcmp (options, vers->options) == 0)) + { + return DIFF_SAME; + } + if (use_rev1 == NULL + && (vers->vn_user[0] != '0' || vers->vn_user[1] != '\0')) + { + if (vers->vn_user[0] == '-') + use_rev1 = xstrdup (vers->vn_user + 1); + else + use_rev1 = xstrdup (vers->vn_user); + } + } + + /* If we already know that the file is being added or removed, + then we don't want to do an actual file comparison here. */ + if (empty_file != DIFF_DIFFERENT) + return empty_file; + + /* + * with 0 or 1 -r option specified, run a quick diff to see if we + * should bother with it at all. + */ + + retcode = RCS_cmp_file (vers->srcfile, use_rev1, + *options ? options : vers->options, + finfo->file); + + return retcode == 0 ? DIFF_SAME : DIFF_DIFFERENT; +} diff --git a/gnu/dist/cvs/src/edit.c b/gnu/dist/cvs/src/edit.c new file mode 100644 index 00000000000..8bc8468aa17 --- /dev/null +++ b/gnu/dist/cvs/src/edit.c @@ -0,0 +1,1125 @@ +/* Implementation for "cvs edit", "cvs watch on", and related commands + + 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, 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. */ + +#include "cvs.h" +#include "getline.h" +#include "watch.h" +#include "edit.h" +#include "fileattr.h" + +static int watch_onoff PROTO ((int, char **)); + +static int setting_default; +static int turning_on; + +static int setting_tedit; +static int setting_tunedit; +static int setting_tcommit; + +static int onoff_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +onoff_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + fileattr_set (finfo->file, "_watched", turning_on ? "" : NULL); + return 0; +} + +static int onoff_filesdoneproc PROTO ((void *, int, char *, char *, List *)); + +static int +onoff_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + char *repository; + char *update_dir; + List *entries; +{ + if (setting_default) + fileattr_set (NULL, "_watched", turning_on ? "" : NULL); + return err; +} + +static int +watch_onoff (argc, argv) + int argc; + char **argv; +{ + int c; + int local = 0; + int err; + + optind = 0; + while ((c = getopt (argc, argv, "+lR")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (watch_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + + ign_setup (); + + if (local) + send_arg ("-l"); + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server (turning_on ? "watch-on\012" : "watch-off\012", 0); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + setting_default = (argc <= 0); + + lock_tree_for_write (argc, argv, local, 0); + + err = start_recursion (onoff_fileproc, onoff_filesdoneproc, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 0); + + Lock_Cleanup (); + return err; +} + +int +watch_on (argc, argv) + int argc; + char **argv; +{ + turning_on = 1; + return watch_onoff (argc, argv); +} + +int +watch_off (argc, argv) + int argc; + char **argv; +{ + turning_on = 0; + return watch_onoff (argc, argv); +} + +static int dummy_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +dummy_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + /* This is a pretty hideous hack, but the gist of it is that recurse.c + won't call notify_check unless there is a fileproc, so we can't just + pass NULL for fileproc. */ + return 0; +} + +static int ncheck_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +/* Check for and process notifications. Local only. I think that doing + this as a fileproc is the only way to catch all the + cases (e.g. foo/bar.c), even though that means checking over and over + for the same CVSADM_NOTIFY file which we removed the first time we + processed the directory. */ + +static int +ncheck_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + int notif_type; + char *filename; + char *val; + char *cp; + char *watches; + + FILE *fp; + char *line = NULL; + size_t line_len = 0; + + /* We send notifications even if noexec. I'm not sure which behavior + is most sensible. */ + + fp = CVS_FOPEN (CVSADM_NOTIFY, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot open %s", CVSADM_NOTIFY); + return 0; + } + + while (getline (&line, &line_len, fp) > 0) + { + notif_type = line[0]; + if (notif_type == '\0') + continue; + filename = line + 1; + cp = strchr (filename, '\t'); + if (cp == NULL) + continue; + *cp++ = '\0'; + val = cp; + cp = strchr (val, '\t'); + if (cp == NULL) + continue; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + continue; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + continue; + *cp++ = '\0'; + watches = cp; + cp = strchr (cp, '\n'); + if (cp == NULL) + continue; + *cp = '\0'; + + notify_do (notif_type, filename, getcaller (), val, watches, + finfo->repository); + } + free (line); + + if (ferror (fp)) + error (0, errno, "cannot read %s", CVSADM_NOTIFY); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + + if ( CVS_UNLINK (CVSADM_NOTIFY) < 0) + error (0, errno, "cannot remove %s", CVSADM_NOTIFY); + + return 0; +} + +static int send_notifications PROTO ((int, char **, int)); + +/* Look through the CVSADM_NOTIFY file and process each item there + accordingly. */ +static int +send_notifications (argc, argv, local) + int argc; + char **argv; + int local; +{ + int err = 0; + +#ifdef CLIENT_SUPPORT + /* OK, we've done everything which needs to happen on the client side. + Now we can try to contact the server; if we fail, then the + notifications stay in CVSADM_NOTIFY to be sent next time. */ + if (client_active) + { + if (strcmp (command_name, "release") != 0) + { + start_server (); + ign_setup (); + } + + err += start_recursion (dummy_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 0); + + send_to_server ("noop\012", 0); + if (strcmp (command_name, "release") == 0) + err += get_server_responses (); + else + err += get_responses_and_close (); + } + else +#endif + { + /* Local. */ + + lock_tree_for_write (argc, argv, local, 0); + err += start_recursion (ncheck_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 0); + Lock_Cleanup (); + } + return err; +} + +static int edit_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +edit_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + FILE *fp; + time_t now; + char *ascnow; + char *basefilename; + + if (noexec) + return 0; + + /* This is a somewhat screwy way to check for this, because it + doesn't help errors other than the nonexistence of the file + (e.g. permissions problems). It might be better to rearrange + the code so that CVSADM_NOTIFY gets written only after the + various actions succeed (but what if only some of them + succeed). */ + if (!isfile (finfo->file)) + { + error (0, 0, "no such file %s; ignored", finfo->fullname); + return 0; + } + + fp = open_file (CVSADM_NOTIFY, "a"); + + (void) time (&now); + ascnow = asctime (gmtime (&now)); + ascnow[24] = '\0'; + fprintf (fp, "E%s\t%s GMT\t%s\t%s\t", finfo->file, + ascnow, hostname, CurDir); + if (setting_tedit) + fprintf (fp, "E"); + if (setting_tunedit) + fprintf (fp, "U"); + if (setting_tcommit) + fprintf (fp, "C"); + fprintf (fp, "\n"); + + if (fclose (fp) < 0) + { + if (finfo->update_dir[0] == '\0') + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + else + error (0, errno, "cannot close %s/%s", finfo->update_dir, + CVSADM_NOTIFY); + } + + xchmod (finfo->file, 1); + + /* Now stash the file away in CVSADM so that unedit can revert even if + it can't communicate with the server. We stash away a writable + copy so that if the user removes the working file, then restores it + with "cvs update" (which clears _editors but does not update + CVSADM_BASE), then a future "cvs edit" can still win. */ + /* Could save a system call by only calling mkdir_if_needed if + trying to create the output file fails. But copy_file isn't + set up to facilitate that. */ + mkdir_if_needed (CVSADM_BASE); + basefilename = xmalloc (10 + sizeof CVSADM_BASE + strlen (finfo->file)); + strcpy (basefilename, CVSADM_BASE); + strcat (basefilename, "/"); + strcat (basefilename, finfo->file); + copy_file (finfo->file, basefilename); + free (basefilename); + + { + Node *node; + + node = findnode_fn (finfo->entries, finfo->file); + if (node != NULL) + base_register (finfo, ((Entnode *) node->data)->version); + } + + return 0; +} + +static const char *const edit_usage[] = +{ + "Usage: %s %s [-lR] [files...]\n", + "-l: Local directory only, not recursive\n", + "-R: Process directories recursively\n", + "-a: Specify what actions for temporary watch, one of\n", + " edit,unedit,commit,all,none\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +edit (argc, argv) + int argc; + char **argv; +{ + int local = 0; + int c; + int err; + int a_omitted; + + if (argc == -1) + usage (edit_usage); + + a_omitted = 1; + setting_tedit = 0; + setting_tunedit = 0; + setting_tcommit = 0; + optind = 0; + while ((c = getopt (argc, argv, "+lRa:")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'a': + a_omitted = 0; + if (strcmp (optarg, "edit") == 0) + setting_tedit = 1; + else if (strcmp (optarg, "unedit") == 0) + setting_tunedit = 1; + else if (strcmp (optarg, "commit") == 0) + setting_tcommit = 1; + else if (strcmp (optarg, "all") == 0) + { + setting_tedit = 1; + setting_tunedit = 1; + setting_tcommit = 1; + } + else if (strcmp (optarg, "none") == 0) + { + setting_tedit = 0; + setting_tunedit = 0; + setting_tcommit = 0; + } + else + usage (edit_usage); + break; + case '?': + default: + usage (edit_usage); + break; + } + } + argc -= optind; + argv += optind; + + if (a_omitted) + { + setting_tedit = 1; + setting_tunedit = 1; + setting_tcommit = 1; + } + + /* No need to readlock since we aren't doing anything to the + repository. */ + err = start_recursion (edit_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 0); + + err += send_notifications (argc, argv, local); + + return err; +} + +static int unedit_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +unedit_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + FILE *fp; + time_t now; + char *ascnow; + char *basefilename; + + if (noexec) + return 0; + + basefilename = xmalloc (10 + sizeof CVSADM_BASE + strlen (finfo->file)); + strcpy (basefilename, CVSADM_BASE); + strcat (basefilename, "/"); + strcat (basefilename, finfo->file); + if (!isfile (basefilename)) + { + /* This file apparently was never cvs edit'd (e.g. we are uneditting + a directory where only some of the files were cvs edit'd. */ + free (basefilename); + return 0; + } + + if (xcmp (finfo->file, basefilename) != 0) + { + printf ("%s has been modified; revert changes? ", finfo->fullname); + if (!yesno ()) + { + /* "no". */ + free (basefilename); + return 0; + } + } + rename_file (basefilename, finfo->file); + free (basefilename); + + fp = open_file (CVSADM_NOTIFY, "a"); + + (void) time (&now); + ascnow = asctime (gmtime (&now)); + ascnow[24] = '\0'; + fprintf (fp, "U%s\t%s GMT\t%s\t%s\t\n", finfo->file, + ascnow, hostname, CurDir); + + if (fclose (fp) < 0) + { + if (finfo->update_dir[0] == '\0') + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + else + error (0, errno, "cannot close %s/%s", finfo->update_dir, + CVSADM_NOTIFY); + } + + /* Now update the revision number in CVS/Entries from CVS/Baserev. + The basic idea here is that we are reverting to the revision + that the user edited. If we wanted "cvs update" to update + CVS/Base as we go along (so that an unedit could revert to the + current repository revision), we would need: + + update (or all send_files?) (client) needs to send revision in + new Entry-base request. update (server/local) needs to check + revision against repository and send new Update-base response + (like Update-existing in that the file already exists. While + we are at it, might try to clean up the syntax by having the + mode only in a "Mode" response, not in the Update-base itself). */ + { + char *baserev; + Node *node; + Entnode *entdata; + + baserev = base_get (finfo); + node = findnode_fn (finfo->entries, finfo->file); + /* The case where node is NULL probably should be an error or + something, but I don't want to think about it too hard right + now. */ + if (node != NULL) + { + entdata = (Entnode *) node->data; + if (baserev == NULL) + { + /* This can only happen if the CVS/Baserev file got + corrupted. We suspect it might be possible if the + user interrupts CVS, although I haven't verified + that. */ + error (0, 0, "%s not mentioned in %s", finfo->fullname, + CVSADM_BASEREV); + + /* Since we don't know what revision the file derives from, + keeping it around would be asking for trouble. */ + if (unlink_file (finfo->file) < 0) + error (0, errno, "cannot remove %s", finfo->fullname); + + /* This is cheesy, in a sense; why shouldn't we do the + update for the user? However, doing that would require + contacting the server, so maybe this is OK. */ + error (0, 0, "run update to complete the unedit"); + return 0; + } + Register (finfo->entries, finfo->file, baserev, entdata->timestamp, + entdata->options, entdata->tag, entdata->date, + entdata->conflict); + } + free (baserev); + base_deregister (finfo); + } + + xchmod (finfo->file, 0); + return 0; +} + +static const char *const unedit_usage[] = +{ + "Usage: %s %s [-lR] [files...]\n", + "-l: Local directory only, not recursive\n", + "-R: Process directories recursively\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +unedit (argc, argv) + int argc; + char **argv; +{ + int local = 0; + int c; + int err; + + if (argc == -1) + usage (unedit_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+lR")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (unedit_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* No need to readlock since we aren't doing anything to the + repository. */ + err = start_recursion (unedit_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 0); + + err += send_notifications (argc, argv, local); + + return err; +} + +void +mark_up_to_date (file) + char *file; +{ + char *base; + + /* The file is up to date, so we better get rid of an out of + date file in CVSADM_BASE. */ + base = xmalloc (strlen (file) + 80); + strcpy (base, CVSADM_BASE); + strcat (base, "/"); + strcat (base, file); + if (unlink_file (base) < 0 && ! existence_error (errno)) + error (0, errno, "cannot remove %s", file); + free (base); +} + + +void +editor_set (filename, editor, val) + char *filename; + char *editor; + char *val; +{ + char *edlist; + char *newlist; + + edlist = fileattr_get0 (filename, "_editors"); + newlist = fileattr_modify (edlist, editor, val, '>', ','); + /* If the attributes is unchanged, don't rewrite the attribute file. */ + if (!((edlist == NULL && newlist == NULL) + || (edlist != NULL + && newlist != NULL + && strcmp (edlist, newlist) == 0))) + fileattr_set (filename, "_editors", newlist); + if (edlist != NULL) + free (edlist); + if (newlist != NULL) + free (newlist); +} + +struct notify_proc_args { + /* What kind of notification, "edit", "tedit", etc. */ + char *type; + /* User who is running the command which causes notification. */ + char *who; + /* User to be notified. */ + char *notifyee; + /* File. */ + char *file; +}; + +/* Pass as a static until we get around to fixing Parse_Info to pass along + a void * where we can stash it. */ +static struct notify_proc_args *notify_args; + +static int notify_proc PROTO ((char *repository, char *filter)); + +static int +notify_proc (repository, filter) + char *repository; + char *filter; +{ + FILE *pipefp; + char *prog; + char *expanded_prog; + char *p; + char *q; + char *srepos; + struct notify_proc_args *args = notify_args; + + srepos = Short_Repository (repository); + prog = xmalloc (strlen (filter) + strlen (args->notifyee) + 1); + /* Copy FILTER to PROG, replacing the first occurrence of %s with + the notifyee. We only allocated enough memory for one %s, and I doubt + there is a need for more. */ + for (p = filter, q = prog; *p != '\0'; ++p) + { + if (p[0] == '%') + { + if (p[1] == 's') + { + strcpy (q, args->notifyee); + q += strlen (q); + strcpy (q, p + 2); + q += strlen (q); + break; + } + else + continue; + } + *q++ = *p; + } + *q = '\0'; + + /* FIXME: why are we calling expand_proc? Didn't we already + expand it in Parse_Info, before passing it to notify_proc? */ + expanded_prog = expand_path (prog, "notify", 0); + if (!expanded_prog) + { + free (prog); + return 1; + } + + pipefp = run_popen (expanded_prog, "w"); + if (pipefp == NULL) + { + error (0, errno, "cannot write entry to notify filter: %s", prog); + free (prog); + free (expanded_prog); + return 1; + } + + fprintf (pipefp, "%s %s\n---\n", srepos, args->file); + fprintf (pipefp, "Triggered %s watch on %s\n", args->type, repository); + fprintf (pipefp, "By %s\n", args->who); + + /* Lots more potentially useful information we could add here; see + logfile_write for inspiration. */ + + free (prog); + free (expanded_prog); + return (pclose (pipefp)); +} + +/* FIXME: this function should have a way to report whether there was + an error so that server.c can know whether to report Notified back + to the client. */ +void +notify_do (type, filename, who, val, watches, repository) + int type; + char *filename; + char *who; + char *val; + char *watches; + char *repository; +{ + static struct addremove_args blank; + struct addremove_args args; + char *watchers; + char *p; + char *endp; + char *nextp; + + /* Initialize fields to 0, NULL, or 0.0. */ + args = blank; + switch (type) + { + case 'E': + if (strpbrk (val, ",>;=\n") != NULL) + { + error (0, 0, "invalid character in editor value"); + return; + } + editor_set (filename, who, val); + break; + case 'U': + case 'C': + editor_set (filename, who, NULL); + break; + default: + return; + } + + watchers = fileattr_get0 (filename, "_watchers"); + p = watchers; + while (p != NULL) + { + char *q; + char *endq; + char *nextq; + char *notif; + + endp = strchr (p, '>'); + if (endp == NULL) + break; + nextp = strchr (p, ','); + + if ((size_t)(endp - p) == strlen (who) && strncmp (who, p, endp - p) == 0) + { + /* Don't notify user of their own changes. Would perhaps + be better to check whether it is the same working + directory, not the same user, but that is hairy. */ + p = nextp == NULL ? nextp : nextp + 1; + continue; + } + + /* Now we point q at a string which looks like + "edit+unedit+commit,"... and walk down it. */ + q = endp + 1; + notif = NULL; + while (q != NULL) + { + endq = strchr (q, '+'); + if (endq == NULL || (nextp != NULL && endq > nextp)) + { + if (nextp == NULL) + endq = q + strlen (q); + else + endq = nextp; + nextq = NULL; + } + else + nextq = endq + 1; + + /* If there is a temporary and a regular watch, send a single + notification, for the regular watch. */ + if (type == 'E' && endq - q == 4 && strncmp ("edit", q, 4) == 0) + { + notif = "edit"; + } + else if (type == 'U' + && endq - q == 6 && strncmp ("unedit", q, 6) == 0) + { + notif = "unedit"; + } + else if (type == 'C' + && endq - q == 6 && strncmp ("commit", q, 6) == 0) + { + notif = "commit"; + } + else if (type == 'E' + && endq - q == 5 && strncmp ("tedit", q, 5) == 0) + { + if (notif == NULL) + notif = "temporary edit"; + } + else if (type == 'U' + && endq - q == 7 && strncmp ("tunedit", q, 7) == 0) + { + if (notif == NULL) + notif = "temporary unedit"; + } + else if (type == 'C' + && endq - q == 7 && strncmp ("tcommit", q, 7) == 0) + { + if (notif == NULL) + notif = "temporary commit"; + } + q = nextq; + } + if (nextp != NULL) + ++nextp; + + if (notif != NULL) + { + struct notify_proc_args args; + size_t len = endp - p; + FILE *fp; + char *usersname; + char *line = NULL; + size_t line_len = 0; + + args.notifyee = NULL; + usersname = xmalloc (strlen (CVSroot_directory) + + sizeof CVSROOTADM + + sizeof CVSROOTADM_USERS + + 20); + strcpy (usersname, CVSroot_directory); + strcat (usersname, "/"); + strcat (usersname, CVSROOTADM); + strcat (usersname, "/"); + strcat (usersname, CVSROOTADM_USERS); + fp = CVS_FOPEN (usersname, "r"); + if (fp == NULL && !existence_error (errno)) + error (0, errno, "cannot read %s", usersname); + if (fp != NULL) + { + while (getline (&line, &line_len, fp) >= 0) + { + if (strncmp (line, p, len) == 0 + && line[len] == ':') + { + char *cp; + args.notifyee = xstrdup (line + len + 1); + + /* There may or may not be more + colon-separated fields added to this in the + future; in any case, we ignore them right + now, and if there are none we make sure to + chop off the final newline, if any. */ + cp = strpbrk (args.notifyee, ":\n"); + + if (cp != NULL) + *cp = '\0'; + break; + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", usersname); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", usersname); + } + free (usersname); + if (line != NULL) + free (line); + + if (args.notifyee == NULL) + { + args.notifyee = xmalloc (endp - p + 1); + strncpy (args.notifyee, p, endp - p); + args.notifyee[endp - p] = '\0'; + } + + notify_args = &args; + args.type = notif; + args.who = who; + args.file = filename; + + (void) Parse_Info (CVSROOTADM_NOTIFY, repository, notify_proc, 1); + free (args.notifyee); + } + + p = nextp; + } + if (watchers != NULL) + free (watchers); + + switch (type) + { + case 'E': + if (*watches == 'E') + { + args.add_tedit = 1; + ++watches; + } + if (*watches == 'U') + { + args.add_tunedit = 1; + ++watches; + } + if (*watches == 'C') + { + args.add_tcommit = 1; + } + watch_modify_watchers (filename, &args); + break; + case 'U': + case 'C': + args.remove_temp = 1; + watch_modify_watchers (filename, &args); + break; + } +} + +#ifdef CLIENT_SUPPORT +/* Check and send notifications. This is only for the client. */ +void +notify_check (repository, update_dir) + char *repository; + char *update_dir; +{ + FILE *fp; + char *line = NULL; + size_t line_len = 0; + + if (! server_started) + /* We are in the midst of a command which is not to talk to + the server (e.g. the first phase of a cvs edit). Just chill + out, we'll catch the notifications on the flip side. */ + return; + + /* We send notifications even if noexec. I'm not sure which behavior + is most sensible. */ + + fp = CVS_FOPEN (CVSADM_NOTIFY, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot open %s", CVSADM_NOTIFY); + return; + } + while (getline (&line, &line_len, fp) > 0) + { + int notif_type; + char *filename; + char *val; + char *cp; + + notif_type = line[0]; + if (notif_type == '\0') + continue; + filename = line + 1; + cp = strchr (filename, '\t'); + if (cp == NULL) + continue; + *cp++ = '\0'; + val = cp; + + client_notify (repository, update_dir, filename, notif_type, val); + } + if (line) + free (line); + if (ferror (fp)) + error (0, errno, "cannot read %s", CVSADM_NOTIFY); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + + /* Leave the CVSADM_NOTIFY file there, until the server tells us it + has dealt with it. */ +} +#endif /* CLIENT_SUPPORT */ + + +static const char *const editors_usage[] = +{ + "Usage: %s %s [-lR] [files...]\n", + "\t-l\tProcess this directory only (not recursive).\n", + "\t-R\tProcess directories recursively.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +static int editors_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +editors_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *them; + char *p; + + them = fileattr_get0 (finfo->file, "_editors"); + if (them == NULL) + return 0; + + cvs_output (finfo->fullname, 0); + + p = them; + while (1) + { + cvs_output ("\t", 1); + while (*p != '>' && *p != '\0') + cvs_output (p++, 1); + if (*p == '\0') + { + /* Only happens if attribute is misformed. */ + cvs_output ("\n", 1); + break; + } + ++p; + cvs_output ("\t", 1); + while (1) + { + while (*p != '+' && *p != ',' && *p != '\0') + cvs_output (p++, 1); + if (*p == '\0') + { + cvs_output ("\n", 1); + goto out; + } + if (*p == ',') + { + ++p; + break; + } + ++p; + cvs_output ("\t", 1); + } + cvs_output ("\n", 1); + } + out:; + return 0; +} + +int +editors (argc, argv) + int argc; + char **argv; +{ + int local = 0; + int c; + + if (argc == -1) + usage (editors_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+lR")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (editors_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + ign_setup (); + + if (local) + send_arg ("-l"); + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server ("editors\012", 0); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + return start_recursion (editors_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 1, (char *)NULL, + 0); +} diff --git a/gnu/dist/cvs/src/entries.c b/gnu/dist/cvs/src/entries.c new file mode 100644 index 00000000000..14163bb8ae6 --- /dev/null +++ b/gnu/dist/cvs/src/entries.c @@ -0,0 +1,1185 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Entries file to Files file + * + * Creates the file Files containing the names that comprise the project, from + * the Entries file. + */ + +#include "cvs.h" +#include "getline.h" + +static Node *AddEntryNode PROTO((List * list, Entnode *entnode)); + +static Entnode *fgetentent PROTO((FILE *, char *, int *)); +static int fputentent PROTO((FILE *, Entnode *)); + +static Entnode *subdir_record PROTO((int, const char *, const char *)); + +static FILE *entfile; +static char *entfilename; /* for error messages */ + +/* + * Construct an Entnode + */ +static Entnode *Entnode_Create PROTO ((enum ent_type, const char *, + const char *, const char *, + const char *, const char *, + const char *, const char *)); + +static Entnode * +Entnode_Create(type, user, vn, ts, options, tag, date, ts_conflict) + enum ent_type type; + const char *user; + const char *vn; + const char *ts; + const char *options; + const char *tag; + const char *date; + const char *ts_conflict; +{ + Entnode *ent; + + /* Note that timestamp and options must be non-NULL */ + ent = (Entnode *) xmalloc (sizeof (Entnode)); + ent->type = type; + ent->user = xstrdup (user); + ent->version = xstrdup (vn); + ent->timestamp = xstrdup (ts ? ts : ""); + ent->options = xstrdup (options ? options : ""); + ent->tag = xstrdup (tag); + ent->date = xstrdup (date); + ent->conflict = xstrdup (ts_conflict); + + return ent; +} + +/* + * Destruct an Entnode + */ +static void Entnode_Destroy PROTO ((Entnode *)); + +static void +Entnode_Destroy (ent) + Entnode *ent; +{ + free (ent->user); + free (ent->version); + free (ent->timestamp); + free (ent->options); + if (ent->tag) + free (ent->tag); + if (ent->date) + free (ent->date); + if (ent->conflict) + free (ent->conflict); + free (ent); +} + +/* + * Write out the line associated with a node of an entries file + */ +static int write_ent_proc PROTO ((Node *, void *)); +static int +write_ent_proc (node, closure) + Node *node; + void *closure; +{ + Entnode *entnode; + + entnode = (Entnode *) node->data; + + if (closure != NULL && entnode->type != ENT_FILE) + *(int *) closure = 1; + + if (fputentent(entfile, entnode)) + error (1, errno, "cannot write %s", entfilename); + + return (0); +} + +/* + * write out the current entries file given a list, making a backup copy + * first of course + */ +static void +write_entries (list) + List *list; +{ + int sawdir; + + sawdir = 0; + + /* open the new one and walk the list writing entries */ + entfilename = CVSADM_ENTBAK; + entfile = CVS_FOPEN (entfilename, "w+"); + if (entfile == NULL) + { + /* Make this a warning, not an error. For example, one user might + have checked out a working directory which, for whatever reason, + contains an Entries.Log file. A second user, without write access + to that working directory, might want to do a "cvs log". The + problem rewriting Entries shouldn't affect the ability of "cvs log" + to work, although the warning is probably a good idea so that + whether Entries gets rewritten is not an inexplicable process. */ + /* FIXME: should be including update_dir in message. */ + error (0, errno, "cannot rewrite %s", entfilename); + + /* Now just return. We leave the Entries.Log file around. As far + as I know, there is never any data lying around in 'list' that + is not in Entries.Log at this time (if there is an error writing + Entries.Log that is a separate problem). */ + return; + } + + (void) walklist (list, write_ent_proc, (void *) &sawdir); + if (! sawdir) + { + struct stickydirtag *sdtp; + + /* We didn't write out any directories. Check the list + private data to see whether subdirectory information is + known. If it is, we need to write out an empty D line. */ + sdtp = (struct stickydirtag *) list->list->data; + if (sdtp == NULL || sdtp->subdirs) + if (fprintf (entfile, "D\n") < 0) + error (1, errno, "cannot write %s", entfilename); + } + if (fclose (entfile) == EOF) + error (1, errno, "error closing %s", entfilename); + + /* now, atomically (on systems that support it) rename it */ + rename_file (entfilename, CVSADM_ENT); + + /* now, remove the log file */ + if (unlink_file (CVSADM_ENTLOG) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", CVSADM_ENTLOG); +} + +/* + * Removes the argument file from the Entries file if necessary. + */ +void +Scratch_Entry (list, fname) + List *list; + char *fname; +{ + Node *node; + + if (trace) + (void) fprintf (stderr, "%s-> Scratch_Entry(%s)\n", + CLIENT_SERVER_STR, fname); + + /* hashlookup to see if it is there */ + if ((node = findnode_fn (list, fname)) != NULL) + { + if (!noexec) + { + entfilename = CVSADM_ENTLOG; + entfile = open_file (entfilename, "a"); + + if (fprintf (entfile, "R ") < 0) + error (1, errno, "cannot write %s", entfilename); + + write_ent_proc (node, NULL); + + if (fclose (entfile) == EOF) + error (1, errno, "error closing %s", entfilename); + } + + delnode (node); /* delete the node */ + +#ifdef SERVER_SUPPORT + if (server_active) + server_scratch (fname); +#endif + } +} + +/* + * Enters the given file name/version/time-stamp into the Entries file, + * removing the old entry first, if necessary. + */ +void +Register (list, fname, vn, ts, options, tag, date, ts_conflict) + List *list; + char *fname; + char *vn; + char *ts; + char *options; + char *tag; + char *date; + char *ts_conflict; +{ + Entnode *entnode; + Node *node; + +#ifdef SERVER_SUPPORT + if (server_active) + { + server_register (fname, vn, ts, options, tag, date, ts_conflict); + } +#endif + + if (trace) + { + (void) fprintf (stderr, "%s-> Register(%s, %s, %s%s%s, %s, %s %s)\n", + CLIENT_SERVER_STR, + fname, vn, ts ? ts : "", + ts_conflict ? "+" : "", ts_conflict ? ts_conflict : "", + options, tag ? tag : "", date ? date : ""); + } + + entnode = Entnode_Create (ENT_FILE, fname, vn, ts, options, tag, date, + ts_conflict); + node = AddEntryNode (list, entnode); + + if (!noexec) + { + entfilename = CVSADM_ENTLOG; + entfile = CVS_FOPEN (entfilename, "a"); + + if (entfile == NULL) + { + /* Warning, not error, as in write_entries. */ + /* FIXME-update-dir: should be including update_dir in message. */ + error (0, errno, "cannot open %s", entfilename); + return; + } + + if (fprintf (entfile, "A ") < 0) + error (1, errno, "cannot write %s", entfilename); + + write_ent_proc (node, NULL); + + if (fclose (entfile) == EOF) + error (1, errno, "error closing %s", entfilename); + } +} + +/* + * Node delete procedure for list-private sticky dir tag/date info + */ +static void +freesdt (p) + Node *p; +{ + struct stickydirtag *sdtp; + + sdtp = (struct stickydirtag *) p->data; + if (sdtp->tag) + free (sdtp->tag); + if (sdtp->date) + free (sdtp->date); + free ((char *) sdtp); +} + +/* Return the next real Entries line. On end of file, returns NULL. + On error, prints an error message and returns NULL. */ + +static Entnode * +fgetentent(fpin, cmd, sawdir) + FILE *fpin; + char *cmd; + int *sawdir; +{ + Entnode *ent; + char *line; + size_t line_chars_allocated; + register char *cp; + enum ent_type type; + char *l, *user, *vn, *ts, *options; + char *tag_or_date, *tag, *date, *ts_conflict; + int line_length; + + line = NULL; + line_chars_allocated = 0; + + ent = NULL; + while ((line_length = getline (&line, &line_chars_allocated, fpin)) > 0) + { + l = line; + + /* If CMD is not NULL, we are reading an Entries.Log file. + Each line in the Entries.Log file starts with a single + character command followed by a space. For backward + compatibility, the absence of a space indicates an add + command. */ + if (cmd != NULL) + { + if (l[1] != ' ') + *cmd = 'A'; + else + { + *cmd = l[0]; + l += 2; + } + } + + type = ENT_FILE; + + if (l[0] == 'D') + { + type = ENT_SUBDIR; + *sawdir = 1; + ++l; + /* An empty D line is permitted; it is a signal that this + Entries file lists all known subdirectories. */ + } + + if (l[0] != '/') + continue; + + user = l + 1; + if ((cp = strchr (user, '/')) == NULL) + continue; + *cp++ = '\0'; + vn = cp; + if ((cp = strchr (vn, '/')) == NULL) + continue; + *cp++ = '\0'; + ts = cp; + if ((cp = strchr (ts, '/')) == NULL) + continue; + *cp++ = '\0'; + options = cp; + if ((cp = strchr (options, '/')) == NULL) + continue; + *cp++ = '\0'; + tag_or_date = cp; + if ((cp = strchr (tag_or_date, '\n')) == NULL) + continue; + *cp = '\0'; + tag = (char *) NULL; + date = (char *) NULL; + if (*tag_or_date == 'T') + tag = tag_or_date + 1; + else if (*tag_or_date == 'D') + date = tag_or_date + 1; + + if ((ts_conflict = strchr (ts, '+'))) + *ts_conflict++ = '\0'; + + /* + * XXX - Convert timestamp from old format to new format. + * + * If the timestamp doesn't match the file's current + * mtime, we'd have to generate a string that doesn't + * match anyways, so cheat and base it on the existing + * string; it doesn't have to match the same mod time. + * + * For an unmodified file, write the correct timestamp. + */ + { + struct stat sb; + if (strlen (ts) > 30 && CVS_STAT (user, &sb) == 0) + { + char *c = ctime (&sb.st_mtime); + + if (!strncmp (ts + 25, c, 24)) + ts = time_stamp (user); + else + { + ts += 24; + ts[0] = '*'; + } + } + } + + ent = Entnode_Create (type, user, vn, ts, options, tag, date, + ts_conflict); + break; + } + + if (line_length < 0 && !feof (fpin)) + error (0, errno, "cannot read entries file"); + + free (line); + return ent; +} + +static int +fputentent(fp, p) + FILE *fp; + Entnode *p; +{ + switch (p->type) + { + case ENT_FILE: + break; + case ENT_SUBDIR: + if (fprintf (fp, "D") < 0) + return 1; + break; + } + + if (fprintf (fp, "/%s/%s/%s", p->user, p->version, p->timestamp) < 0) + return 1; + if (p->conflict) + { + if (fprintf (fp, "+%s", p->conflict) < 0) + return 1; + } + if (fprintf (fp, "/%s/", p->options) < 0) + return 1; + + if (p->tag) + { + if (fprintf (fp, "T%s\n", p->tag) < 0) + return 1; + } + else if (p->date) + { + if (fprintf (fp, "D%s\n", p->date) < 0) + return 1; + } + else + { + if (fprintf (fp, "\n") < 0) + return 1; + } + + return 0; +} + + +/* Read the entries file into a list, hashing on the file name. + + UPDATE_DIR is the name of the current directory, for use in error + messages, or NULL if not known (that is, noone has gotten around + to updating the caller to pass in the information). */ +List * +Entries_Open (aflag, update_dir) + int aflag; + char *update_dir; +{ + List *entries; + struct stickydirtag *sdtp = NULL; + Entnode *ent; + char *dirtag, *dirdate; + int dirnonbranch; + int do_rewrite = 0; + FILE *fpin; + int sawdir; + + /* get a fresh list... */ + entries = getlist (); + + /* + * Parse the CVS/Tag file, to get any default tag/date settings. Use + * list-private storage to tuck them away for Version_TS(). + */ + ParseTag (&dirtag, &dirdate, &dirnonbranch); + if (aflag || dirtag || dirdate) + { + sdtp = (struct stickydirtag *) xmalloc (sizeof (*sdtp)); + memset ((char *) sdtp, 0, sizeof (*sdtp)); + sdtp->aflag = aflag; + sdtp->tag = xstrdup (dirtag); + sdtp->date = xstrdup (dirdate); + sdtp->nonbranch = dirnonbranch; + + /* feed it into the list-private area */ + entries->list->data = (char *) sdtp; + entries->list->delproc = freesdt; + } + + sawdir = 0; + + fpin = CVS_FOPEN (CVSADM_ENT, "r"); + if (fpin == NULL) + { + if (update_dir != NULL) + error (0, 0, "in directory %s:", update_dir); + error (0, errno, "cannot open %s for reading", CVSADM_ENT); + } + else + { + while ((ent = fgetentent (fpin, (char *) NULL, &sawdir)) != NULL) + { + (void) AddEntryNode (entries, ent); + } + + if (fclose (fpin) < 0) + /* FIXME-update-dir: should include update_dir in message. */ + error (0, errno, "cannot close %s", CVSADM_ENT); + } + + fpin = CVS_FOPEN (CVSADM_ENTLOG, "r"); + if (fpin != NULL) + { + char cmd; + Node *node; + + while ((ent = fgetentent (fpin, &cmd, &sawdir)) != NULL) + { + switch (cmd) + { + case 'A': + (void) AddEntryNode (entries, ent); + break; + case 'R': + node = findnode_fn (entries, ent->user); + if (node != NULL) + delnode (node); + Entnode_Destroy (ent); + break; + default: + /* Ignore unrecognized commands. */ + break; + } + } + do_rewrite = 1; + if (fclose (fpin) < 0) + /* FIXME-update-dir: should include update_dir in message. */ + error (0, errno, "cannot close %s", CVSADM_ENTLOG); + } + + /* Update the list private data to indicate whether subdirectory + information is known. Nonexistent list private data is taken + to mean that it is known. */ + if (sdtp != NULL) + sdtp->subdirs = sawdir; + else if (! sawdir) + { + sdtp = (struct stickydirtag *) xmalloc (sizeof (*sdtp)); + memset ((char *) sdtp, 0, sizeof (*sdtp)); + sdtp->subdirs = 0; + entries->list->data = (char *) sdtp; + entries->list->delproc = freesdt; + } + + if (do_rewrite && !noexec) + write_entries (entries); + + /* clean up and return */ + if (dirtag) + free (dirtag); + if (dirdate) + free (dirdate); + return (entries); +} + +void +Entries_Close(list) + List *list; +{ + if (list) + { + if (!noexec) + { + if (isfile (CVSADM_ENTLOG)) + write_entries (list); + } + dellist(&list); + } +} + + +/* + * Free up the memory associated with the data section of an ENTRIES type + * node + */ +static void +Entries_delproc (node) + Node *node; +{ + Entnode *p; + + p = (Entnode *) node->data; + Entnode_Destroy(p); +} + +/* + * Get an Entries file list node, initialize it, and add it to the specified + * list + */ +static Node * +AddEntryNode (list, entdata) + List *list; + Entnode *entdata; +{ + Node *p; + + /* was it already there? */ + if ((p = findnode_fn (list, entdata->user)) != NULL) + { + /* take it out */ + delnode (p); + } + + /* get a node and fill in the regular stuff */ + p = getnode (); + p->type = ENTRIES; + p->delproc = Entries_delproc; + + /* this one gets a key of the name for hashing */ + /* FIXME This results in duplicated data --- the hash package shouldn't + assume that the key is dynamically allocated. The user's free proc + should be responsible for freeing the key. */ + p->key = xstrdup (entdata->user); + p->data = (char *) entdata; + + /* put the node into the list */ + addnode (list, p); + return (p); +} + +/* + * Write out/Clear the CVS/Tag file. + */ +void +WriteTag (dir, tag, date, nonbranch, update_dir, repository) + char *dir; + char *tag; + char *date; + int nonbranch; + char *update_dir; + char *repository; +{ + FILE *fout; + char *tmp; + + if (noexec) + return; + + tmp = xmalloc ((dir ? strlen (dir) : 0) + + sizeof (CVSADM_TAG) + + 10); + if (dir == NULL) + (void) strcpy (tmp, CVSADM_TAG); + else + (void) sprintf (tmp, "%s/%s", dir, CVSADM_TAG); + + if (tag || date) + { + fout = open_file (tmp, "w+"); + if (tag) + { + if (nonbranch) + { + if (fprintf (fout, "N%s\n", tag) < 0) + error (1, errno, "write to %s failed", tmp); + } + else + { + if (fprintf (fout, "T%s\n", tag) < 0) + error (1, errno, "write to %s failed", tmp); + } + } + else + { + if (fprintf (fout, "D%s\n", date) < 0) + error (1, errno, "write to %s failed", tmp); + } + if (fclose (fout) == EOF) + error (1, errno, "cannot close %s", tmp); + } + else + if (unlink_file (tmp) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove %s", tmp); + free (tmp); +#ifdef SERVER_SUPPORT + if (server_active) + server_set_sticky (update_dir, repository, tag, date, nonbranch); +#endif +} + +/* Parse the CVS/Tag file for the current directory. + + If it contains a date, sets *DATEP to the date in a newly malloc'd + string, *TAGP to NULL, and *NONBRANCHP to an unspecified value. + + If it contains a branch tag, sets *TAGP to the tag in a newly + malloc'd string, *NONBRANCHP to 0, and *DATEP to NULL. + + If it contains a nonbranch tag, sets *TAGP to the tag in a newly + malloc'd string, *NONBRANCHP to 1, and *DATEP to NULL. + + If it does not exist, or contains something unrecognized by this + version of CVS, set *DATEP and *TAGP to NULL and *NONBRANCHP to + an unspecified value. + + If there is an error, print an error message, set *DATEP and *TAGP + to NULL, and return. */ +void +ParseTag (tagp, datep, nonbranchp) + char **tagp; + char **datep; + int *nonbranchp; +{ + FILE *fp; + + if (tagp) + *tagp = (char *) NULL; + if (datep) + *datep = (char *) NULL; + /* Always store a value here, even in the 'D' case where the value + is unspecified. Shuts up tools which check for references to + uninitialized memory. */ + if (nonbranchp != NULL) + *nonbranchp = 0; + fp = CVS_FOPEN (CVSADM_TAG, "r"); + if (fp) + { + char *line; + int line_length; + size_t line_chars_allocated; + + line = NULL; + line_chars_allocated = 0; + + if ((line_length = getline (&line, &line_chars_allocated, fp)) > 0) + { + /* Remove any trailing newline. */ + if (line[line_length - 1] == '\n') + line[--line_length] = '\0'; + switch (*line) + { + case 'T': + if (tagp != NULL) + *tagp = xstrdup (line + 1); + break; + case 'D': + if (datep != NULL) + *datep = xstrdup (line + 1); + break; + case 'N': + if (tagp != NULL) + *tagp = xstrdup (line + 1); + if (nonbranchp != NULL) + *nonbranchp = 1; + break; + default: + /* Silently ignore it; it may have been + written by a future version of CVS which extends the + syntax. */ + break; + } + } + + if (line_length < 0) + { + /* FIXME-update-dir: should include update_dir in messages. */ + if (feof (fp)) + error (0, 0, "cannot read %s: end of file", CVSADM_TAG); + else + error (0, errno, "cannot read %s", CVSADM_TAG); + } + + if (fclose (fp) < 0) + /* FIXME-update-dir: should include update_dir in message. */ + error (0, errno, "cannot close %s", CVSADM_TAG); + + free (line); + } + else if (!existence_error (errno)) + /* FIXME-update-dir: should include update_dir in message. */ + error (0, errno, "cannot open %s", CVSADM_TAG); +} + +/* + * This is called if all subdirectory information is known, but there + * aren't any subdirectories. It records that fact in the list + * private data. + */ + +void +Subdirs_Known (entries) + List *entries; +{ + struct stickydirtag *sdtp; + + /* If there is no list private data, that means that the + subdirectory information is known. */ + sdtp = (struct stickydirtag *) entries->list->data; + if (sdtp != NULL && ! sdtp->subdirs) + { + FILE *fp; + + sdtp->subdirs = 1; + if (!noexec) + { + /* Create Entries.Log so that Entries_Close will do something. */ + entfilename = CVSADM_ENTLOG; + fp = CVS_FOPEN (entfilename, "a"); + if (fp == NULL) + { + int save_errno = errno; + + /* As in subdir_record, just silently skip the whole thing + if there is no CVSADM directory. */ + if (! isdir (CVSADM)) + return; + error (1, save_errno, "cannot open %s", entfilename); + } + else + { + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", entfilename); + } + } + } +} + +/* Record subdirectory information. */ + +static Entnode * +subdir_record (cmd, parent, dir) + int cmd; + const char *parent; + const char *dir; +{ + Entnode *entnode; + + /* None of the information associated with a directory is + currently meaningful. */ + entnode = Entnode_Create (ENT_SUBDIR, dir, "", "", "", + (char *) NULL, (char *) NULL, + (char *) NULL); + + if (!noexec) + { + if (parent == NULL) + entfilename = CVSADM_ENTLOG; + else + { + entfilename = xmalloc (strlen (parent) + + sizeof CVSADM_ENTLOG + + 10); + sprintf (entfilename, "%s/%s", parent, CVSADM_ENTLOG); + } + + entfile = CVS_FOPEN (entfilename, "a"); + if (entfile == NULL) + { + int save_errno = errno; + + /* It is not an error if there is no CVS administration + directory. Permitting this case simplifies some + calling code. */ + + if (parent == NULL) + { + if (! isdir (CVSADM)) + return entnode; + } + else + { + sprintf (entfilename, "%s/%s", parent, CVSADM); + if (! isdir (entfilename)) + { + free (entfilename); + entfilename = NULL; + return entnode; + } + } + + error (1, save_errno, "cannot open %s", entfilename); + } + + if (fprintf (entfile, "%c ", cmd) < 0) + error (1, errno, "cannot write %s", entfilename); + + if (fputentent (entfile, entnode) != 0) + error (1, errno, "cannot write %s", entfilename); + + if (fclose (entfile) == EOF) + error (1, errno, "error closing %s", entfilename); + + if (parent != NULL) + { + free (entfilename); + entfilename = NULL; + } + } + + return entnode; +} + +/* + * Record the addition of a new subdirectory DIR in PARENT. PARENT + * may be NULL, which means the current directory. ENTRIES is the + * current entries list; it may be NULL, which means that it need not + * be updated. + */ + +void +Subdir_Register (entries, parent, dir) + List *entries; + const char *parent; + const char *dir; +{ + Entnode *entnode; + + /* Ignore attempts to register ".". These can happen in the + server code. */ + if (dir[0] == '.' && dir[1] == '\0') + return; + + entnode = subdir_record ('A', parent, dir); + + if (entries != NULL && (parent == NULL || strcmp (parent, ".") == 0)) + (void) AddEntryNode (entries, entnode); + else + Entnode_Destroy (entnode); +} + +/* + * Record the removal of a subdirectory. The arguments are the same + * as for Subdir_Register. + */ + +void +Subdir_Deregister (entries, parent, dir) + List *entries; + const char *parent; + const char *dir; +{ + Entnode *entnode; + + entnode = subdir_record ('R', parent, dir); + Entnode_Destroy (entnode); + + if (entries != NULL && (parent == NULL || strcmp (parent, ".") == 0)) + { + Node *p; + + p = findnode_fn (entries, dir); + if (p != NULL) + delnode (p); + } +} + + + +/* OK, the following base_* code tracks the revisions of the files in + CVS/Base. We do this in a file CVS/Baserev. Separate from + CVS/Entries because it needs to go in separate data structures + anyway (the name in Entries must be unique), so this seemed + cleaner. The business of rewriting the whole file in + base_deregister and base_register is the kind of thing we used to + do for Entries and which turned out to be slow, which is why there + is now the Entries.Log machinery. So maybe from that point of + view it is a mistake to do this separately from Entries, I dunno. + + We also need something analogous for: + + 1. CVS/Template (so we can update the Template file automagically + without the user needing to check out a new working directory). + Updating would probably print a message (that part might be + optional, although probably it should be visible because not all + cvs commands would make the update happen and so it is a + user-visible behavior). Constructing version number for template + is a bit hairy (base it on the timestamp on the server? Or see if + the template is in checkoutlist and if yes use its versioning and + if no don't version it?).... + + 2. cvsignore (need to keep a copy in the working directory to do + "cvs release" on the client side; see comment at src/release.c + (release). Would also allow us to stop needing Questionable. */ + +enum base_walk { + /* Set the revision for FILE to *REV. */ + BASE_REGISTER, + /* Get the revision for FILE and put it in a newly malloc'd string + in *REV, or put NULL if not mentioned. */ + BASE_GET, + /* Remove FILE. */ + BASE_DEREGISTER +}; + +static void base_walk PROTO ((enum base_walk, struct file_info *, char **)); + +/* Read through the lines in CVS/Baserev, taking the actions as documented + for CODE. */ + +static void +base_walk (code, finfo, rev) + enum base_walk code; + struct file_info *finfo; + char **rev; +{ + FILE *fp; + char *line; + size_t line_allocated; + FILE *newf; + char *baserev_fullname; + char *baserevtmp_fullname; + + line = NULL; + line_allocated = 0; + newf = NULL; + + /* First compute the fullnames for the error messages. This + computation probably should be broken out into a separate function, + as recurse.c does it too and places like Entries_Open should be + doing it. */ + baserev_fullname = xmalloc (sizeof (CVSADM_BASEREV) + + strlen (finfo->update_dir) + + 2); + baserev_fullname[0] = '\0'; + baserevtmp_fullname = xmalloc (sizeof (CVSADM_BASEREVTMP) + + strlen (finfo->update_dir) + + 2); + baserevtmp_fullname[0] = '\0'; + if (finfo->update_dir[0] != '\0') + { + strcat (baserev_fullname, finfo->update_dir); + strcat (baserev_fullname, "/"); + strcat (baserevtmp_fullname, finfo->update_dir); + strcat (baserevtmp_fullname, "/"); + } + strcat (baserev_fullname, CVSADM_BASEREV); + strcat (baserevtmp_fullname, CVSADM_BASEREVTMP); + + fp = CVS_FOPEN (CVSADM_BASEREV, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + { + error (0, errno, "cannot open %s for reading", baserev_fullname); + goto out; + } + } + + switch (code) + { + case BASE_REGISTER: + case BASE_DEREGISTER: + newf = CVS_FOPEN (CVSADM_BASEREVTMP, "w"); + if (newf == NULL) + { + error (0, errno, "cannot open %s for writing", + baserevtmp_fullname); + goto out; + } + break; + case BASE_GET: + *rev = NULL; + break; + } + + if (fp != NULL) + { + while (getline (&line, &line_allocated, fp) >= 0) + { + char *linefile; + char *p; + char *linerev; + + if (line[0] != 'B') + /* Ignore, for future expansion. */ + continue; + + linefile = line + 1; + p = strchr (linefile, '/'); + if (p == NULL) + /* Syntax error, ignore. */ + continue; + linerev = p + 1; + p = strchr (linerev, '/'); + if (p == NULL) + continue; + + linerev[-1] = '\0'; + if (fncmp (linefile, finfo->file) == 0) + { + switch (code) + { + case BASE_REGISTER: + case BASE_DEREGISTER: + /* Don't copy over the old entry, we don't want it. */ + break; + case BASE_GET: + *p = '\0'; + *rev = xstrdup (linerev); + *p = '/'; + goto got_it; + } + } + else + { + linerev[-1] = '/'; + switch (code) + { + case BASE_REGISTER: + case BASE_DEREGISTER: + if (fprintf (newf, "%s\n", line) < 0) + error (0, errno, "error writing %s", + baserevtmp_fullname); + break; + case BASE_GET: + break; + } + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", baserev_fullname); + } + got_it: + + if (code == BASE_REGISTER) + { + if (fprintf (newf, "B%s/%s/\n", finfo->file, *rev) < 0) + error (0, errno, "error writing %s", + baserevtmp_fullname); + } + + out: + + if (line != NULL) + free (line); + + if (fp != NULL) + { + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", baserev_fullname); + } + if (newf != NULL) + { + if (fclose (newf) < 0) + error (0, errno, "cannot close %s", baserevtmp_fullname); + rename_file (CVSADM_BASEREVTMP, CVSADM_BASEREV); + } + + free (baserev_fullname); + free (baserevtmp_fullname); +} + +/* Return, in a newly malloc'd string, the revision for FILE in CVS/Baserev, + or NULL if not listed. */ + +char * +base_get (finfo) + struct file_info *finfo; +{ + char *rev; + base_walk (BASE_GET, finfo, &rev); + return rev; +} + +/* Set the revision for FILE to REV. */ + +void +base_register (finfo, rev) + struct file_info *finfo; + char *rev; +{ + base_walk (BASE_REGISTER, finfo, &rev); +} + +/* Remove FILE. */ + +void +base_deregister (finfo) + struct file_info *finfo; +{ + base_walk (BASE_DEREGISTER, finfo, NULL); +} diff --git a/gnu/dist/cvs/src/error.c b/gnu/dist/cvs/src/error.c new file mode 100644 index 00000000000..bdb519d5a09 --- /dev/null +++ b/gnu/dist/cvs/src/error.c @@ -0,0 +1,236 @@ +/* error.c -- error handler for noninteractive utilities + Copyright (C) 1990-1992 Free Software Foundation, Inc. + + 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, 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. */ + +/* David MacKenzie */ +/* Brian Berliner added support for CVS */ + +#include "cvs.h" + +#include <stdio.h> + +/* If non-zero, error will use the CVS protocol to stdout to report error + messages. This will only be set in the CVS server parent process; + most other code is run via do_cvs_command, which forks off a child + process and packages up its stderr in the protocol. */ +int error_use_protocol; + +#ifdef HAVE_VPRINTF + +#ifdef __STDC__ +#include <stdarg.h> +#define VA_START(args, lastarg) va_start(args, lastarg) +#else /* ! __STDC__ */ +#include <varargs.h> +#define VA_START(args, lastarg) va_start(args) +#endif /* __STDC__ */ + +#else /* ! HAVE_VPRINTF */ + +#ifdef HAVE_DOPRNT +#define va_alist args +#define va_dcl int args; +#else /* ! HAVE_DOPRNT */ +#define va_alist a1, a2, a3, a4, a5, a6, a7, a8 +#define va_dcl char *a1, *a2, *a3, *a4, *a5, *a6, *a7, *a8; +#endif /* HAVE_DOPRNT */ + +#endif /* HAVE_VPRINTF */ + +#if STDC_HEADERS +#include <stdlib.h> +#include <string.h> +#else /* ! STDC_HEADERS */ +#ifdef __STDC__ +void exit(int status); +#else /* ! __STDC__ */ +void exit (); +#endif /* __STDC__ */ +#endif /* STDC_HEADERS */ + +#ifndef strerror +extern char *strerror (); +#endif + +void +error_exit PROTO ((void)) +{ + Lock_Cleanup(); +#ifdef SERVER_SUPPORT + if (server_active) + server_cleanup (0); +#endif +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + exit (EXIT_FAILURE); +} + +/* Print the program name and error message MESSAGE, which is a printf-style + format string with optional args. This is a very limited printf subset: + %s, %d, %c, %x and %% only (without anything between the % and the s, + d, &c). Callers who want something fancier can use sprintf. + + If ERRNUM is nonzero, print its corresponding system error message. + Exit with status EXIT_FAILURE if STATUS is nonzero. If MESSAGE is "", + no need to print a message. + + I think this is largely cleaned up to the point where it does the right + thing for the server, whether the normal server_active (child process) + case or the error_use_protocol (parent process) case. The one exception + is that STATUS nonzero for error_use_protocol probably doesn't work yet; + in that case still need to use the pending_error machinery in server.c. + + error() does not molest errno; some code (e.g. Entries_Open) depends + on being able to say something like: + error (0, 0, "foo"); + error (0, errno, "bar"); + + */ + +/* VARARGS */ +void +#if defined (__STDC__) +error (int status, int errnum, const char *message, ...) +#else +error (status, errnum, message, va_alist) + int status; + int errnum; + const char *message; + va_dcl +#endif +{ + int save_errno = errno; + + if (message[0] != '\0') + { + va_list args; + const char *p; + char *q; + char *str; + int num; + unsigned int unum; + int ch; + unsigned char buf[100]; + + cvs_outerr (program_name, 0); + if (command_name && *command_name) + { + cvs_outerr (" ", 1); + if (status != 0) + cvs_outerr ("[", 1); + cvs_outerr (command_name, 0); + if (status != 0) + cvs_outerr (" aborted]", 0); + } + cvs_outerr (": ", 2); + + VA_START (args, message); + p = message; + while ((q = strchr (p, '%')) != NULL) + { + static const char msg[] = + "\ninternal error: bad % in error()\n"; + if (q - p > 0) + cvs_outerr (p, q - p); + + switch (q[1]) + { + case 's': + str = va_arg (args, char *); + cvs_outerr (str, strlen (str)); + break; + case 'd': + num = va_arg (args, int); + sprintf (buf, "%d", num); + cvs_outerr (buf, strlen (buf)); + break; + case 'x': + unum = va_arg (args, unsigned int); + sprintf (buf, "%x", unum); + cvs_outerr (buf, strlen (buf)); + break; + case 'c': + ch = va_arg (args, int); + buf[0] = ch; + cvs_outerr (buf, 1); + break; + case '%': + cvs_outerr ("%", 1); + break; + default: + cvs_outerr (msg, sizeof (msg) - 1); + /* Don't just keep going, because q + 1 might point to the + terminating '\0'. */ + goto out; + } + p = q + 2; + } + cvs_outerr (p, strlen (p)); + out: + va_end (args); + + if (errnum != 0) + { + cvs_outerr (": ", 2); + cvs_outerr (strerror (errnum), 0); + } + cvs_outerr ("\n", 1); + } + + if (status) + error_exit (); + errno = save_errno; +} + +/* Print the program name and error message MESSAGE, which is a printf-style + format string with optional args to the file specified by FP. + If ERRNUM is nonzero, print its corresponding system error message. + Exit with status EXIT_FAILURE if STATUS is nonzero. */ +/* VARARGS */ +void +#if defined (HAVE_VPRINTF) && defined (__STDC__) +fperrmsg (FILE *fp, int status, int errnum, char *message, ...) +#else +fperrmsg (fp, status, errnum, message, va_alist) + FILE *fp; + int status; + int errnum; + char *message; + va_dcl +#endif +{ +#ifdef HAVE_VPRINTF + va_list args; +#endif + + fprintf (fp, "%s: ", program_name); +#ifdef HAVE_VPRINTF + VA_START (args, message); + vfprintf (fp, message, args); + va_end (args); +#else +#ifdef HAVE_DOPRNT + _doprnt (message, &args, fp); +#else + fprintf (fp, message, a1, a2, a3, a4, a5, a6, a7, a8); +#endif +#endif + if (errnum) + fprintf (fp, ": %s", strerror (errnum)); + putc ('\n', fp); + fflush (fp); + if (status) + error_exit (); +} diff --git a/gnu/dist/cvs/src/expand_path.c b/gnu/dist/cvs/src/expand_path.c new file mode 100644 index 00000000000..25e561cc0e6 --- /dev/null +++ b/gnu/dist/cvs/src/expand_path.c @@ -0,0 +1,335 @@ +/* expand_path.c -- expand environmental variables in passed in string + * + * The main routine is expand_path(), it is the routine that handles + * the '~' character in four forms: + * ~name + * ~name/ + * ~/ + * ~ + * and handles environment variables contained within the pathname + * which are defined by: + * ${var_name} (var_name is the name of the environ variable) + * $var_name (var_name ends w/ non-alphanumeric char other than '_') + */ + +#include "cvs.h" +#include <sys/types.h> + +static char *expand_variable PROTO((char *env, char *file, int line)); + + +/* User variables. */ + +List *variable_list = NULL; + +static void variable_delproc PROTO ((Node *)); + +static void +variable_delproc (node) + Node *node; +{ + free (node->data); +} + +/* Currently used by -s option; we might want a way to set user + variables in a file in the $CVSROOT/CVSROOT directory too. */ + +void +variable_set (nameval) + char *nameval; +{ + char *p; + char *name; + Node *node; + + p = nameval; + while (isalnum ((unsigned char) *p) || *p == '_') + ++p; + if (*p != '=') + error (1, 0, "illegal character in user variable name in %s", nameval); + if (p == nameval) + error (1, 0, "empty user variable name in %s", nameval); + name = xmalloc (p - nameval + 1); + strncpy (name, nameval, p - nameval); + name[p - nameval] = '\0'; + /* Make p point to the value. */ + ++p; + if (strchr (p, '\012') != NULL) + error (1, 0, "linefeed in user variable value in %s", nameval); + + if (variable_list == NULL) + variable_list = getlist (); + + node = findnode (variable_list, name); + if (node == NULL) + { + node = getnode (); + node->type = VARIABLE; + node->delproc = variable_delproc; + node->key = name; + node->data = xstrdup (p); + (void) addnode (variable_list, node); + } + else + { + /* Replace the old value. For example, this means that -s + options on the command line override ones from .cvsrc. */ + free (node->data); + node->data = xstrdup (p); + free (name); + } +} + +/* This routine will expand the pathname to account for ~ and $ + characters as described above. Returns a pointer to a newly + malloc'd string. If an error occurs, an error message is printed + via error() and NULL is returned. FILE and LINE are the filename + and linenumber to include in the error message. FILE must point + to something; LINE can be zero to indicate the line number is not + known. */ +char * +expand_path (name, file, line) + char *name; + char *file; + int line; +{ + char *s; + char *d; + + char *mybuf = NULL; + size_t mybuf_size = 0; + char *buf = NULL; + size_t buf_size = 0; + + size_t doff; + + char *result; + + /* Sorry this routine is so ugly; it is a head-on collision + between the `traditional' unix *d++ style and the need to + dynamically allocate. It would be much cleaner (and probably + faster, not that this is a bottleneck for CVS) with more use of + strcpy & friends, but I haven't taken the effort to rewrite it + thusly. */ + + /* First copy from NAME to MYBUF, expanding $<foo> as we go. */ + s = name; + d = mybuf; + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + while ((*d++ = *s)) + { + if (*s++ == '$') + { + char *p = d; + char *e; + int flag = (*s == '{'); + + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + for (; (*d++ = *s); s++) + { + if (flag + ? *s =='}' + : isalnum ((unsigned char) *s) == 0 && *s != '_') + break; + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + } + *--d = '\0'; + e = expand_variable (&p[flag], file, line); + + if (e) + { + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + for (d = &p[-1]; (*d++ = *e++);) + { + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + } + --d; + if (flag && *s) + s++; + } + else + /* expand_variable has already printed an error message. */ + goto error_exit; + } + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + } + doff = d - mybuf; + expand_string (&mybuf, &mybuf_size, doff + 1); + d = mybuf + doff; + *d = '\0'; + + /* Then copy from MYBUF to BUF, expanding ~. */ + s = mybuf; + d = buf; + /* If you don't want ~username ~/ to be expanded simply remove + * This entire if statement including the else portion + */ + if (*s++ == '~') + { + char *t; + char *p=s; + if (*s=='/' || *s==0) + t = get_homedir (); + else + { +#ifdef GETPWNAM_MISSING + for (; *p!='/' && *p; p++) + ; + *p = 0; + if (line != 0) + error (0, 0, + "%s:%d:tilde expansion not supported on this system", + file, line); + else + error (0, 0, "%s:tilde expansion not supported on this system", + file); + return NULL; +#else + struct passwd *ps; + for (; *p!='/' && *p; p++) + ; + *p = 0; + ps = getpwnam (s); + if (ps == 0) + { + if (line != 0) + error (0, 0, "%s:%d: no such user %s", + file, line, s); + else + error (0, 0, "%s: no such user %s", file, s); + return NULL; + } + t = ps->pw_dir; +#endif + } + if (t == NULL) + error (1, 0, "cannot find home directory"); + + doff = d - buf; + expand_string (&buf, &buf_size, doff + 1); + d = buf + doff; + while ((*d++ = *t++)) + { + doff = d - buf; + expand_string (&buf, &buf_size, doff + 1); + d = buf + doff; + } + --d; + if (*p == 0) + *p = '/'; /* always add / */ + s=p; + } + else + --s; + /* Kill up to here */ + doff = d - buf; + expand_string (&buf, &buf_size, doff + 1); + d = buf + doff; + while ((*d++ = *s++)) + { + doff = d - buf; + expand_string (&buf, &buf_size, doff + 1); + d = buf + doff; + } + doff = d - buf; + expand_string (&buf, &buf_size, doff + 1); + d = buf + doff; + *d = '\0'; + + /* OK, buf contains the value we want to return. Clean up and return + it. */ + free (mybuf); + /* Save a little memory with xstrdup; buf will tend to allocate + more than it needs to. */ + result = xstrdup (buf); + free (buf); + return result; + + error_exit: + if (mybuf != NULL) + free (mybuf); + if (buf != NULL) + free (buf); + return NULL; +} + +static char * +expand_variable (name, file, line) + char *name; + char *file; + int line; +{ + if (strcmp (name, CVSROOT_ENV) == 0) + return CVSroot_original; + else if (strcmp (name, "RCSBIN") == 0) + { + error (0, 0, "RCSBIN internal variable is no longer supported"); + return NULL; + } + else if (strcmp (name, EDITOR1_ENV) == 0) + return Editor; + else if (strcmp (name, EDITOR2_ENV) == 0) + return Editor; + else if (strcmp (name, EDITOR3_ENV) == 0) + return Editor; + else if (strcmp (name, "USER") == 0) + return getcaller (); + else if (isalpha ((unsigned char) name[0])) + { + /* These names are reserved for future versions of CVS, + so that is why it is an error. */ + if (line != 0) + error (0, 0, "%s:%d: no such internal variable $%s", + file, line, name); + else + error (0, 0, "%s: no such internal variable $%s", + file, name); + return NULL; + } + else if (name[0] == '=') + { + Node *node; + /* Crazy syntax for a user variable. But we want + *something* that lets the user name a user variable + anything he wants, without interference from + (existing or future) internal variables. */ + node = findnode (variable_list, name + 1); + if (node == NULL) + { + if (line != 0) + error (0, 0, "%s:%d: no such user variable ${%s}", + file, line, name); + else + error (0, 0, "%s: no such user variable ${%s}", + file, name); + return NULL; + } + return node->data; + } + else + { + /* It is an unrecognized character. We return an error to + reserve these for future versions of CVS; it is plausible + that various crazy syntaxes might be invented for inserting + information about revisions, branches, etc. */ + if (line != 0) + error (0, 0, "%s:%d: unrecognized variable syntax %s", + file, line, name); + else + error (0, 0, "%s: unrecognized variable syntax %s", + file, name); + return NULL; + } +} diff --git a/gnu/dist/cvs/src/fileattr.c b/gnu/dist/cvs/src/fileattr.c new file mode 100644 index 00000000000..4042d2a528e --- /dev/null +++ b/gnu/dist/cvs/src/fileattr.c @@ -0,0 +1,652 @@ +/* Implementation for file attribute munging features. + + 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, 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. */ + +#include "cvs.h" +#include "getline.h" +#include "fileattr.h" +#include <assert.h> + +static void fileattr_read PROTO ((void)); +static int writeattr_proc PROTO ((Node *, void *)); + +/* Where to look for CVSREP_FILEATTR. */ +static char *fileattr_stored_repos; + +/* The in-memory attributes. */ +static List *attrlist; +static char *fileattr_default_attrs; +/* We have already tried to read attributes and failed in this directory + (for example, there is no CVSREP_FILEATTR file). */ +static int attr_read_attempted; + +/* Have the in-memory attributes been modified since we read them? */ +static int attrs_modified; + +/* More in-memory attributes: linked list of unrecognized + fileattr lines. We pass these on unchanged. */ +struct unrecog { + char *line; + struct unrecog *next; +}; +static struct unrecog *unrecog_head; + +/* Note that if noone calls fileattr_get, this is very cheap. No stat(), + no open(), no nothing. */ +void +fileattr_startdir (repos) + char *repos; +{ + assert (fileattr_stored_repos == NULL); + fileattr_stored_repos = xstrdup (repos); + assert (attrlist == NULL); + attr_read_attempted = 0; + assert (unrecog_head == NULL); +} + +static void +fileattr_delproc (node) + Node *node; +{ + assert (node->data != NULL); + free (node->data); + node->data = NULL; +} + +/* Read all the attributes for the current directory into memory. */ +static void +fileattr_read () +{ + char *fname; + FILE *fp; + char *line = NULL; + size_t line_len = 0; + + /* If there are no attributes, don't waste time repeatedly looking + for the CVSREP_FILEATTR file. */ + if (attr_read_attempted) + return; + + /* If NULL was passed to fileattr_startdir, then it isn't kosher to look + at attributes. */ + assert (fileattr_stored_repos != NULL); + + fname = xmalloc (strlen (fileattr_stored_repos) + + 1 + + sizeof (CVSREP_FILEATTR) + + 1); + + strcpy (fname, fileattr_stored_repos); + strcat (fname, "/"); + strcat (fname, CVSREP_FILEATTR); + + attr_read_attempted = 1; + fp = CVS_FOPEN (fname, FOPEN_BINARY_READ); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot read %s", fname); + free (fname); + return; + } + attrlist = getlist (); + while (1) { + int nread; + nread = getline (&line, &line_len, fp); + if (nread < 0) + break; + /* Remove trailing newline. */ + line[nread - 1] = '\0'; + if (line[0] == 'F') + { + char *p; + Node *newnode; + + p = strchr (line, '\t'); + if (p == NULL) + error (1, 0, + "file attribute database corruption: tab missing in %s", + fname); + *p++ = '\0'; + newnode = getnode (); + newnode->type = FILEATTR; + newnode->delproc = fileattr_delproc; + newnode->key = xstrdup (line + 1); + newnode->data = xstrdup (p); + if (addnode (attrlist, newnode) != 0) + /* If the same filename appears twice in the file, discard + any line other than the first for that filename. This + is the way that CVS has behaved since file attributes + were first introduced. */ + free (newnode); + } + else if (line[0] == 'D') + { + char *p; + /* Currently nothing to skip here, but for future expansion, + ignore anything located here. */ + p = strchr (line, '\t'); + if (p == NULL) + error (1, 0, + "file attribute database corruption: tab missing in %s", + fname); + ++p; + fileattr_default_attrs = xstrdup (p); + } + else + { + /* Unrecognized type, we want to just preserve the line without + changing it, for future expansion. */ + struct unrecog *new; + + new = (struct unrecog *) xmalloc (sizeof (struct unrecog)); + new->line = xstrdup (line); + new->next = unrecog_head; + unrecog_head = new; + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", fname); + if (line != NULL) + free (line); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + attrs_modified = 0; + free (fname); +} + +char * +fileattr_get (filename, attrname) + const char *filename; + const char *attrname; +{ + Node *node; + size_t attrname_len = strlen (attrname); + char *p; + + if (attrlist == NULL) + fileattr_read (); + if (attrlist == NULL) + /* Either nothing has any attributes, or fileattr_read already printed + an error message. */ + return NULL; + + if (filename == NULL) + p = fileattr_default_attrs; + else + { + node = findnode (attrlist, filename); + if (node == NULL) + /* A file not mentioned has no attributes. */ + return NULL; + p = node->data; + } + while (p) + { + if (strncmp (attrname, p, attrname_len) == 0 + && p[attrname_len] == '=') + { + /* Found it. */ + return p + attrname_len + 1; + } + p = strchr (p, ';'); + if (p == NULL) + break; + ++p; + } + /* The file doesn't have this attribute. */ + return NULL; +} + +char * +fileattr_get0 (filename, attrname) + const char *filename; + const char *attrname; +{ + char *cp; + char *cpend; + char *retval; + + cp = fileattr_get (filename, attrname); + if (cp == NULL) + return NULL; + cpend = strchr (cp, ';'); + if (cpend == NULL) + cpend = cp + strlen (cp); + retval = xmalloc (cpend - cp + 1); + strncpy (retval, cp, cpend - cp); + retval[cpend - cp] = '\0'; + return retval; +} + +char * +fileattr_modify (list, attrname, attrval, namevalsep, entsep) + char *list; + const char *attrname; + const char *attrval; + int namevalsep; + int entsep; +{ + char *retval; + char *rp; + size_t attrname_len = strlen (attrname); + + /* Portion of list before the attribute to be replaced. */ + char *pre; + char *preend; + /* Portion of list after the attribute to be replaced. */ + char *post; + + char *p; + char *p2; + + p = list; + pre = list; + preend = NULL; + /* post is NULL unless set otherwise. */ + post = NULL; + p2 = NULL; + if (list != NULL) + { + while (1) { + p2 = strchr (p, entsep); + if (p2 == NULL) + { + p2 = p + strlen (p); + if (preend == NULL) + preend = p2; + } + else + ++p2; + if (strncmp (attrname, p, attrname_len) == 0 + && p[attrname_len] == namevalsep) + { + /* Found it. */ + preend = p; + if (preend > list) + /* Don't include the preceding entsep. */ + --preend; + + post = p2; + } + if (p2[0] == '\0') + break; + p = p2; + } + } + if (post == NULL) + post = p2; + + if (preend == pre && attrval == NULL && post == p2) + return NULL; + + retval = xmalloc ((preend - pre) + + 1 + + (attrval == NULL ? 0 : (attrname_len + 1 + + strlen (attrval))) + + 1 + + (p2 - post) + + 1); + if (preend != pre) + { + strncpy (retval, pre, preend - pre); + rp = retval + (preend - pre); + if (attrval != NULL) + *rp++ = entsep; + *rp = '\0'; + } + else + retval[0] = '\0'; + if (attrval != NULL) + { + strcat (retval, attrname); + rp = retval + strlen (retval); + *rp++ = namevalsep; + strcpy (rp, attrval); + } + if (post != p2) + { + rp = retval + strlen (retval); + if (preend != pre || attrval != NULL) + *rp++ = entsep; + strncpy (rp, post, p2 - post); + rp += p2 - post; + *rp = '\0'; + } + return retval; +} + +void +fileattr_set (filename, attrname, attrval) + const char *filename; + const char *attrname; + const char *attrval; +{ + Node *node; + char *p; + + if (filename == NULL) + { + p = fileattr_modify (fileattr_default_attrs, attrname, attrval, + '=', ';'); + if (fileattr_default_attrs != NULL) + free (fileattr_default_attrs); + fileattr_default_attrs = p; + attrs_modified = 1; + return; + } + if (attrlist == NULL) + fileattr_read (); + if (attrlist == NULL) + { + /* Not sure this is a graceful way to handle things + in the case where fileattr_read was unable to read the file. */ + /* No attributes existed previously. */ + attrlist = getlist (); + } + + node = findnode (attrlist, filename); + if (node == NULL) + { + if (attrval == NULL) + /* Attempt to remove an attribute which wasn't there. */ + return; + + /* First attribute for this file. */ + node = getnode (); + node->type = FILEATTR; + node->delproc = fileattr_delproc; + node->key = xstrdup (filename); + node->data = xmalloc (strlen (attrname) + 1 + strlen (attrval) + 1); + strcpy (node->data, attrname); + strcat (node->data, "="); + strcat (node->data, attrval); + addnode (attrlist, node); + } + + p = fileattr_modify (node->data, attrname, attrval, '=', ';'); + if (p == NULL) + delnode (node); + else + { + free (node->data); + node->data = p; + } + + attrs_modified = 1; +} + +char * +fileattr_getall (filename) + const char *filename; +{ + Node *node; + char *p; + + if (attrlist == NULL) + fileattr_read (); + if (attrlist == NULL) + /* Either nothing has any attributes, or fileattr_read already printed + an error message. */ + return NULL; + + if (filename == NULL) + p = fileattr_default_attrs; + else + { + node = findnode (attrlist, filename); + if (node == NULL) + /* A file not mentioned has no attributes. */ + return NULL; + p = node->data; + } + return xstrdup (p); +} + +void +fileattr_setall (filename, attrs) + const char *filename; + const char *attrs; +{ + Node *node; + + if (filename == NULL) + { + if (fileattr_default_attrs != NULL) + free (fileattr_default_attrs); + fileattr_default_attrs = xstrdup (attrs); + attrs_modified = 1; + return; + } + if (attrlist == NULL) + fileattr_read (); + if (attrlist == NULL) + { + /* Not sure this is a graceful way to handle things + in the case where fileattr_read was unable to read the file. */ + /* No attributes existed previously. */ + attrlist = getlist (); + } + + node = findnode (attrlist, filename); + if (node == NULL) + { + /* The file had no attributes. Add them if we have any to add. */ + if (attrs != NULL) + { + node = getnode (); + node->type = FILEATTR; + node->delproc = fileattr_delproc; + node->key = xstrdup (filename); + node->data = xstrdup (attrs); + addnode (attrlist, node); + } + } + else + { + if (attrs == NULL) + delnode (node); + else + { + free (node->data); + node->data = xstrdup (attrs); + } + } + + attrs_modified = 1; +} + +void +fileattr_newfile (filename) + const char *filename; +{ + Node *node; + + if (attrlist == NULL) + fileattr_read (); + + if (fileattr_default_attrs == NULL) + return; + + if (attrlist == NULL) + { + /* Not sure this is a graceful way to handle things + in the case where fileattr_read was unable to read the file. */ + /* No attributes existed previously. */ + attrlist = getlist (); + } + + node = getnode (); + node->type = FILEATTR; + node->delproc = fileattr_delproc; + node->key = xstrdup (filename); + node->data = xstrdup (fileattr_default_attrs); + addnode (attrlist, node); + attrs_modified = 1; +} + +static int +writeattr_proc (node, data) + Node *node; + void *data; +{ + FILE *fp = (FILE *)data; + fputs ("F", fp); + fputs (node->key, fp); + fputs ("\t", fp); + fputs (node->data, fp); + fputs ("\012", fp); + return 0; +} + +void +fileattr_write () +{ + FILE *fp; + char *fname; + mode_t omask; + + if (!attrs_modified) + return; + + if (noexec) + return; + + /* If NULL was passed to fileattr_startdir, then it isn't kosher to set + attributes. */ + assert (fileattr_stored_repos != NULL); + + fname = xmalloc (strlen (fileattr_stored_repos) + + 1 + + sizeof (CVSREP_FILEATTR) + + 1); + + strcpy (fname, fileattr_stored_repos); + strcat (fname, "/"); + strcat (fname, CVSREP_FILEATTR); + + if (list_isempty (attrlist) + && fileattr_default_attrs == NULL + && unrecog_head == NULL) + { + /* There are no attributes. */ + if (unlink_file (fname) < 0) + { + if (!existence_error (errno)) + { + error (0, errno, "cannot remove %s", fname); + } + } + + /* Now remove CVSREP directory, if empty. The main reason we bother + is that CVS 1.6 and earlier will choke if a CVSREP directory + exists, so provide the user a graceful way to remove it. */ + strcpy (fname, fileattr_stored_repos); + strcat (fname, "/"); + strcat (fname, CVSREP); + if (CVS_RMDIR (fname) < 0) + { + if (errno != ENOTEMPTY + + /* Don't know why we would be here if there is no CVSREP + directory, but it seemed to be happening anyway, so + check for it. */ + && !existence_error (errno)) + error (0, errno, "cannot remove %s", fname); + } + + free (fname); + return; + } + + omask = umask (cvsumask); + fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE); + if (fp == NULL) + { + if (existence_error (errno)) + { + /* Maybe the CVSREP directory doesn't exist. Try creating it. */ + char *repname; + + repname = xmalloc (strlen (fileattr_stored_repos) + + 1 + + sizeof (CVSREP) + + 1); + strcpy (repname, fileattr_stored_repos); + strcat (repname, "/"); + strcat (repname, CVSREP); + + if (CVS_MKDIR (repname, 0777) < 0 && errno != EEXIST) + { + error (0, errno, "cannot make directory %s", repname); + (void) umask (omask); + free (repname); + return; + } + free (repname); + + fp = CVS_FOPEN (fname, FOPEN_BINARY_WRITE); + } + if (fp == NULL) + { + error (0, errno, "cannot write %s", fname); + (void) umask (omask); + return; + } + } + (void) umask (omask); + + /* First write the "F" attributes. */ + walklist (attrlist, writeattr_proc, fp); + + /* Then the "D" attribute. */ + if (fileattr_default_attrs != NULL) + { + fputs ("D\t", fp); + fputs (fileattr_default_attrs, fp); + fputs ("\012", fp); + } + + /* Then any other attributes. */ + while (unrecog_head != NULL) + { + struct unrecog *p; + + p = unrecog_head; + fputs (p->line, fp); + fputs ("\012", fp); + + unrecog_head = p->next; + free (p->line); + free (p); + } + + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + attrs_modified = 0; + free (fname); +} + +void +fileattr_free () +{ + /* Note that attrs_modified will ordinarily be zero, but there are + a few cases in which fileattr_write will fail to zero it (if + noexec is set, or error conditions). This probably is the way + it should be. */ + dellist (&attrlist); + if (fileattr_stored_repos != NULL) + free (fileattr_stored_repos); + fileattr_stored_repos = NULL; + if (fileattr_default_attrs != NULL) + free (fileattr_default_attrs); + fileattr_default_attrs = NULL; +} diff --git a/gnu/dist/cvs/src/filesubr.c b/gnu/dist/cvs/src/filesubr.c new file mode 100644 index 00000000000..f3da62a5efd --- /dev/null +++ b/gnu/dist/cvs/src/filesubr.c @@ -0,0 +1,973 @@ +/* filesubr.c --- subroutines for dealing with files + Jim Blandy <jimb@cyclic.com> + + This file is part of GNU CVS. + + GNU CVS 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, 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. */ + +/* These functions were moved out of subr.c because they need different + definitions under operating systems (like, say, Windows NT) with different + file system semantics. */ + +#include "cvs.h" + +static int deep_remove_dir PROTO((const char *path)); + +/* + * Copies "from" to "to". + */ +void +copy_file (from, to) + const char *from; + const char *to; +{ + struct stat sb; + struct utimbuf t; + int fdin, fdout; + + if (trace) + (void) fprintf (stderr, "%s-> copy(%s,%s)\n", + CLIENT_SERVER_STR, from, to); + if (noexec) + return; + + /* If the file to be copied is a link or a device, then just create + the new link or device appropriately. */ + if (islink (from)) + { + char *source = xreadlink (from); + symlink (source, to); + free (source); + return; + } + + if (isdevice (from)) + { + if (stat (from, &sb) < 0) + error (1, errno, "cannot stat %s", from); + mknod (to, sb.st_mode, sb.st_rdev); + } + else + { + /* Not a link or a device... probably a regular file. */ + if ((fdin = open (from, O_RDONLY)) < 0) + error (1, errno, "cannot open %s for copying", from); + if (fstat (fdin, &sb) < 0) + error (1, errno, "cannot fstat %s", from); + if ((fdout = creat (to, (int) sb.st_mode & 07777)) < 0) + error (1, errno, "cannot create %s for copying", to); + if (sb.st_size > 0) + { + char buf[BUFSIZ]; + int n; + + for (;;) + { + n = read (fdin, buf, sizeof(buf)); + if (n == -1) + { +#ifdef EINTR + if (errno == EINTR) + continue; +#endif + error (1, errno, "cannot read file %s for copying", from); + } + else if (n == 0) + break; + + if (write(fdout, buf, n) != n) { + error (1, errno, "cannot write file %s for copying", to); + } + } + +#ifdef HAVE_FSYNC + if (fsync (fdout)) + error (1, errno, "cannot fsync file %s after copying", to); +#endif + } + + if (close (fdin) < 0) + error (0, errno, "cannot close %s", from); + if (close (fdout) < 0) + error (1, errno, "cannot close %s", to); + } + + /* now, set the times for the copied file to match those of the original */ + memset ((char *) &t, 0, sizeof (t)); + t.actime = sb.st_atime; + t.modtime = sb.st_mtime; + (void) utime (to, &t); +} + +/* FIXME-krp: these functions would benefit from caching the char * & + stat buf. */ + +/* + * Returns non-zero if the argument file is a directory, or is a symbolic + * link which points to a directory. + */ +int +isdir (file) + const char *file; +{ + struct stat sb; + + if (stat (file, &sb) < 0) + return (0); + return (S_ISDIR (sb.st_mode)); +} + +/* + * Returns non-zero if the argument file is a symbolic link. + */ +int +islink (file) + const char *file; +{ +#ifdef S_ISLNK + struct stat sb; + + if (CVS_LSTAT (file, &sb) < 0) + return (0); + return (S_ISLNK (sb.st_mode)); +#else + return (0); +#endif +} + +/* + * Returns non-zero if the argument file is a block or + * character special device. + */ +int +isdevice (file) + const char *file; +{ + struct stat sb; + + if (CVS_LSTAT (file, &sb) < 0) + return (0); +#ifdef S_ISBLK + if (S_ISBLK (sb.st_mode)) + return 1; +#endif +#ifdef S_ISCHR + if (S_ISCHR (sb.st_mode)) + return 1; +#endif + return 0; +} + +/* + * Returns non-zero if the argument file exists. + */ +int +isfile (file) + const char *file; +{ + return isaccessible(file, F_OK); +} + +/* + * Returns non-zero if the argument file is readable. + */ +int +isreadable (file) + const char *file; +{ + return isaccessible(file, R_OK); +} + +/* + * Returns non-zero if the argument file is writable. + */ +int +iswritable (file) + const char *file; +{ + return isaccessible(file, W_OK); +} + +/* + * Returns non-zero if the argument file is accessable according to + * mode. If compiled with SETXID_SUPPORT also works if cvs has setxid + * bits set. + */ +int +isaccessible (file, mode) + const char *file; + const int mode; +{ +#ifdef SETXID_SUPPORT + struct stat sb; + int umask = 0; + int gmask = 0; + int omask = 0; + int uid; + + if (stat(file, &sb) == -1) + return 0; + if (mode == F_OK) + return 1; + + uid = geteuid(); + if (uid == 0) /* superuser */ + { + if (mode & X_OK) + return sb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH); + else + return 1; + } + + if (mode & R_OK) + { + umask |= S_IRUSR; + gmask |= S_IRGRP; + omask |= S_IROTH; + } + if (mode & W_OK) + { + umask |= S_IWUSR; + gmask |= S_IWGRP; + omask |= S_IWOTH; + } + if (mode & X_OK) + { + umask |= S_IXUSR; + gmask |= S_IXGRP; + omask |= S_IXOTH; + } + + if (sb.st_uid == uid) + return (sb.st_mode & umask) == umask; + else if (sb.st_gid == getegid()) + return (sb.st_mode & gmask) == gmask; + else + return (sb.st_mode & omask) == omask; +#else + return access(file, mode) == 0; +#endif +} + +/* + * Open a file and die if it fails + */ +FILE * +open_file (name, mode) + const char *name; + const char *mode; +{ + FILE *fp; + + if ((fp = fopen (name, mode)) == NULL) + error (1, errno, "cannot open %s", name); + return (fp); +} + +/* + * Make a directory and die if it fails + */ +void +make_directory (name) + const char *name; +{ + struct stat sb; + + if (stat (name, &sb) == 0 && (!S_ISDIR (sb.st_mode))) + error (0, 0, "%s already exists but is not a directory", name); + if (!noexec && mkdir (name, 0777) < 0) + error (1, errno, "cannot make directory %s", name); +} + +/* + * Make a path to the argument directory, printing a message if something + * goes wrong. + */ +void +make_directories (name) + const char *name; +{ + char *cp; + + if (noexec) + return; + + if (mkdir (name, 0777) == 0 || errno == EEXIST) + return; + if (! existence_error (errno)) + { + error (0, errno, "cannot make path to %s", name); + return; + } + if ((cp = strrchr (name, '/')) == NULL) + return; + *cp = '\0'; + make_directories (name); + *cp++ = '/'; + if (*cp == '\0') + return; + (void) mkdir (name, 0777); +} + +/* Create directory NAME if it does not already exist; fatal error for + other errors. Returns 0 if directory was created; 1 if it already + existed. */ +int +mkdir_if_needed (name) + char *name; +{ + if (mkdir (name, 0777) < 0) + { + if (!(errno == EEXIST + || (errno == EACCES && isdir (name)))) + error (1, errno, "cannot make directory %s", name); + return 1; + } + return 0; +} + +/* + * Change the mode of a file, either adding write permissions, or removing + * all write permissions. Either change honors the current umask setting. + * + * Don't do anything if PreservePermissions is set to `yes'. This may + * have unexpected consequences for some uses of xchmod. + */ +void +xchmod (fname, writable) + char *fname; + int writable; +{ + struct stat sb; + mode_t mode, oumask; + + if (preserve_perms) + return; + + if (stat (fname, &sb) < 0) + { + if (!noexec) + error (0, errno, "cannot stat %s", fname); + return; + } + oumask = umask (0); + (void) umask (oumask); + if (writable) + { + mode = sb.st_mode | (~oumask + & (((sb.st_mode & S_IRUSR) ? S_IWUSR : 0) + | ((sb.st_mode & S_IRGRP) ? S_IWGRP : 0) + | ((sb.st_mode & S_IROTH) ? S_IWOTH : 0))); + } + else + { + mode = sb.st_mode & ~(S_IWRITE | S_IWGRP | S_IWOTH) & ~oumask; + } + + if (trace) + (void) fprintf (stderr, "%s-> chmod(%s,%o)\n", + CLIENT_SERVER_STR, fname, + (unsigned int) mode); + if (noexec) + return; + + if (chmod (fname, mode) < 0) + error (0, errno, "cannot change mode of file %s", fname); +} + +/* + * Rename a file and die if it fails + */ +void +rename_file (from, to) + const char *from; + const char *to; +{ + if (trace) + (void) fprintf (stderr, "%s-> rename(%s,%s)\n", + CLIENT_SERVER_STR, from, to); + if (noexec) + return; + + if (rename (from, to) < 0) + error (1, errno, "cannot rename file %s to %s", from, to); +} + +/* + * unlink a file, if possible. + */ +int +unlink_file (f) + const char *f; +{ + if (trace) + (void) fprintf (stderr, "%s-> unlink(%s)\n", + CLIENT_SERVER_STR, f); + if (noexec) + return (0); + + return (unlink (f)); +} + +/* + * Unlink a file or dir, if possible. If it is a directory do a deep + * removal of all of the files in the directory. Return -1 on error + * (in which case errno is set). + */ +int +unlink_file_dir (f) + const char *f; +{ + struct stat sb; + + if (trace +#ifdef SERVER_SUPPORT + /* This is called by the server parent process in contexts where + it is not OK to send output (e.g. after we sent "ok" to the + client). */ + && !server_active +#endif + ) + (void) fprintf (stderr, "-> unlink_file_dir(%s)\n", f); + + if (noexec) + return (0); + + /* For at least some unices, if root tries to unlink() a directory, + instead of doing something rational like returning EISDIR, + the system will gleefully go ahead and corrupt the filesystem. + So we first call stat() to see if it is OK to call unlink(). This + doesn't quite work--if someone creates a directory between the + call to stat() and the call to unlink(), we'll still corrupt + the filesystem. Where is the Unix Haters Handbook when you need + it? */ + if (stat (f, &sb) < 0) + { + if (existence_error (errno)) + { + /* The file or directory doesn't exist anyhow. */ + return -1; + } + } + else if (S_ISDIR (sb.st_mode)) + return deep_remove_dir (f); + + return unlink (f); +} + +/* Remove a directory and everything it contains. Returns 0 for + * success, -1 for failure (in which case errno is set). + */ + +static int +deep_remove_dir (path) + const char *path; +{ + DIR *dirp; + struct dirent *dp; + + if (rmdir (path) != 0) + { + if (errno == ENOTEMPTY + || errno == EEXIST + /* Ugly workaround for ugly AIX 4.1 (and 3.2) header bug + (it defines ENOTEMPTY and EEXIST to 17 but actually + returns 87). */ + || (ENOTEMPTY == 17 && EEXIST == 17 && errno == 87)) + { + if ((dirp = opendir (path)) == NULL) + /* If unable to open the directory return + * an error + */ + return -1; + + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + char *buf; + + if (strcmp (dp->d_name, ".") == 0 || + strcmp (dp->d_name, "..") == 0) + continue; + + buf = xmalloc (strlen (path) + strlen (dp->d_name) + 5); + sprintf (buf, "%s/%s", path, dp->d_name); + + /* See comment in unlink_file_dir explanation of why we use + isdir instead of just calling unlink and checking the + status. */ + if (isdir(buf)) + { + if (deep_remove_dir(buf)) + { + closedir(dirp); + free (buf); + return -1; + } + } + else + { + if (unlink (buf) != 0) + { + closedir(dirp); + free (buf); + return -1; + } + } + free (buf); + + errno = 0; + } + if (errno != 0) + { + int save_errno = errno; + closedir (dirp); + errno = save_errno; + return -1; + } + closedir (dirp); + return rmdir (path); + } + else + return -1; + } + + /* Was able to remove the directory return 0 */ + return 0; +} + +/* Read NCHARS bytes from descriptor FD into BUF. + Return the number of characters successfully read. + The number returned is always NCHARS unless end-of-file or error. */ +static size_t +block_read (fd, buf, nchars) + int fd; + char *buf; + size_t nchars; +{ + char *bp = buf; + size_t nread; + + do + { + nread = read (fd, bp, nchars); + if (nread == (size_t)-1) + { +#ifdef EINTR + if (errno == EINTR) + continue; +#endif + return (size_t)-1; + } + + if (nread == 0) + break; + + bp += nread; + nchars -= nread; + } while (nchars != 0); + + return bp - buf; +} + + +/* + * Compare "file1" to "file2". Return non-zero if they don't compare exactly. + * If FILE1 and FILE2 are special files, compare their salient characteristics + * (i.e. major/minor device numbers, links, etc. + */ +int +xcmp (file1, file2) + const char *file1; + const char *file2; +{ + char *buf1, *buf2; + struct stat sb1, sb2; + int fd1, fd2; + int ret; + + if (CVS_LSTAT (file1, &sb1) < 0) + error (1, errno, "cannot lstat %s", file1); + if (CVS_LSTAT (file2, &sb2) < 0) + error (1, errno, "cannot lstat %s", file2); + + /* If FILE1 and FILE2 are not the same file type, they are unequal. */ + if ((sb1.st_mode & S_IFMT) != (sb2.st_mode & S_IFMT)) + return 1; + + /* If FILE1 and FILE2 are symlinks, they are equal if they point to + the same thing. */ + if (S_ISLNK (sb1.st_mode) && S_ISLNK (sb2.st_mode)) + { + int result; + buf1 = xreadlink (file1); + buf2 = xreadlink (file2); + result = (strcmp (buf1, buf2) == 0); + free (buf1); + free (buf2); + return result; + } + + /* If FILE1 and FILE2 are devices, they are equal if their device + numbers match. */ + if (S_ISBLK (sb1.st_mode) || S_ISCHR (sb1.st_mode)) + { + if (sb1.st_rdev == sb2.st_rdev) + return 0; + else + return 1; + } + + if ((fd1 = open (file1, O_RDONLY)) < 0) + error (1, errno, "cannot open file %s for comparing", file1); + if ((fd2 = open (file2, O_RDONLY)) < 0) + error (1, errno, "cannot open file %s for comparing", file2); + + /* A generic file compare routine might compare st_dev & st_ino here + to see if the two files being compared are actually the same file. + But that won't happen in CVS, so we won't bother. */ + + if (sb1.st_size != sb2.st_size) + ret = 1; + else if (sb1.st_size == 0) + ret = 0; + else + { + /* FIXME: compute the optimal buffer size by computing the least + common multiple of the files st_blocks field */ + size_t buf_size = 8 * 1024; + size_t read1; + size_t read2; + + buf1 = xmalloc (buf_size); + buf2 = xmalloc (buf_size); + + do + { + read1 = block_read (fd1, buf1, buf_size); + if (read1 == (size_t)-1) + error (1, errno, "cannot read file %s for comparing", file1); + + read2 = block_read (fd2, buf2, buf_size); + if (read2 == (size_t)-1) + error (1, errno, "cannot read file %s for comparing", file2); + + /* assert (read1 == read2); */ + + ret = memcmp(buf1, buf2, read1); + } while (ret == 0 && read1 == buf_size); + + free (buf1); + free (buf2); + } + + (void) close (fd1); + (void) close (fd2); + return (ret); +} + +/* Generate a unique temporary filename. Returns a pointer to a newly + malloc'd string containing the name. Returns successfully or not at + all. */ +/* There are at least three functions for generating temporary + filenames. We use tempnam (SVID 3) if possible, else mktemp (BSD + 4.3), and as last resort tmpnam (POSIX). Reason is that tempnam and + mktemp both allow to specify the directory in which the temporary + file will be created. */ +#ifdef HAVE_TEMPNAM +char * +cvs_temp_name () +{ + char *retval; + + retval = tempnam (Tmpdir, "cvs"); + if (retval == NULL) + error (1, errno, "cannot generate temporary filename"); + /* tempnam returns a pointer to a newly malloc'd string, so there's + no need for a xstrdup */ + return retval; +} +#else +char * +cvs_temp_name () +{ +# ifdef HAVE_MKTEMP + char *value; + char *retval; + + value = xmalloc (strlen (Tmpdir) + 40); + sprintf (value, "%s/%s", Tmpdir, "cvsXXXXXX" ); + retval = mktemp (value); + + if (retval == NULL) + error (1, errno, "cannot generate temporary filename"); + return value; +# else + char value[L_tmpnam + 1]; + char *retval; + + retval = tmpnam (value); + if (retval == NULL) + error (1, errno, "cannot generate temporary filename"); + return xstrdup (value); +# endif +} +#endif + +/* Return non-zero iff FILENAME is absolute. + Trivial under Unix, but more complicated under other systems. */ +int +isabsolute (filename) + const char *filename; +{ + return filename[0] == '/'; +} + +/* + * Return a string (dynamically allocated) with the name of the file to which + * LINK is symlinked. + */ +char * +xreadlink (link) + const char *link; +{ + char *file = NULL; + char *tfile; + int buflen = 128; + int link_name_len; + + if (!islink (link)) + return NULL; + + /* Get the name of the file to which `from' is linked. + FIXME: what portability issues arise here? Are readlink & + ENAMETOOLONG defined on all systems? -twp */ + do + { + file = xrealloc (file, buflen); + link_name_len = readlink (link, file, buflen - 1); + buflen *= 2; + } + while (link_name_len < 0 && errno == ENAMETOOLONG); + + if (link_name_len < 0) + error (1, errno, "cannot readlink %s", link); + + file[link_name_len] = '\0'; + + tfile = xstrdup (file); + free (file); + + return tfile; +} + + +/* Return a pointer into PATH's last component. */ +char * +last_component (path) + char *path; +{ + char *last = strrchr (path, '/'); + + if (last && (last != path)) + return last + 1; + else + return path; +} + +/* Return the home directory. Returns a pointer to storage + managed by this function or its callees (currently getenv). + This function will return the same thing every time it is + called. Returns NULL if there is no home directory. + + Note that for a pserver server, this may return root's home + directory. What typically happens is that upon being started from + inetd, before switching users, the code in cvsrc.c calls + get_homedir which remembers root's home directory in the static + variable. Then the switch happens and get_homedir might return a + directory that we don't even have read or execute permissions for + (which is bad, when various parts of CVS try to read there). One + fix would be to make the value returned by get_homedir only good + until the next call (which would free the old value). Another fix + would be to just always malloc our answer, and let the caller free + it (that is best, because some day we may need to be reentrant). + + The workaround is to put -f in inetd.conf which means that + get_homedir won't get called until after the switch in user ID. + + The whole concept of a "home directory" on the server is pretty + iffy, although I suppose some people probably are relying on it for + .cvsrc and such, in the cases where it works. */ +char * +get_homedir () +{ + static char *home = NULL; + char *env = getenv ("HOME"); + struct passwd *pw; + + if (home != NULL) + return home; + + if (env) + home = env; + else if ((pw = (struct passwd *) getpwuid (getuid ())) + && pw->pw_dir) + home = xstrdup (pw->pw_dir); + else + return 0; + + return home; +} + +/* See cvs.h for description. On unix this does nothing, because the + shell expands the wildcards. */ +void +expand_wild (argc, argv, pargc, pargv) + int argc; + char **argv; + int *pargc; + char ***pargv; +{ + int i; + *pargc = argc; + *pargv = (char **) xmalloc (argc * sizeof (char *)); + for (i = 0; i < argc; ++i) + (*pargv)[i] = xstrdup (argv[i]); +} + +#ifdef SERVER_SUPPORT +/* Case-insensitive string compare. I know that some systems + have such a routine, but I'm not sure I see any reasons for + dealing with the hair of figuring out whether they do (I haven't + looked into whether this is a performance bottleneck; I would guess + not). */ +int +cvs_casecmp (str1, str2) + char *str1; + char *str2; +{ + char *p; + char *q; + int pqdiff; + + p = str1; + q = str2; + while ((pqdiff = tolower (*p) - tolower (*q)) == 0) + { + if (*p == '\0') + return 0; + ++p; + ++q; + } + return pqdiff; +} + +/* Case-insensitive file open. As you can see, this is an expensive + call. We don't regard it as our main strategy for dealing with + case-insensitivity. Returns errno code or 0 for success. Puts the + new file in *FP. NAME and MODE are as for fopen. If PATHP is not + NULL, then put a malloc'd string containing the pathname as found + into *PATHP. *PATHP is only set if the return value is 0. + + Might be cleaner to separate the file finding (which just gives + *PATHP) from the file opening (which the caller can do). For one + thing, might make it easier to know whether to put NAME or *PATHP + into error messages. */ +int +fopen_case (name, mode, fp, pathp) + char *name; + char *mode; + FILE **fp; + char **pathp; +{ + struct dirent *dp; + DIR *dirp; + char *dir; + char *fname; + char *found_name; + int retval; + + /* Separate NAME into directory DIR and filename within the directory + FNAME. */ + dir = xstrdup (name); + fname = strrchr (dir, '/'); + if (fname == NULL) + error (1, 0, "internal error: relative pathname in fopen_case"); + *fname++ = '\0'; + + found_name = NULL; + dirp = CVS_OPENDIR (dir); + if (dirp == NULL) + { + if (existence_error (errno)) + { + /* This can happen if we are looking in the Attic and the Attic + directory does not exist. Return the error to the caller; + they know what to do with it. */ + retval = errno; + goto out; + } + else + { + /* Give a fatal error; that way the error message can be + more specific than if we returned the error to the caller. */ + error (1, errno, "cannot read directory %s", dir); + } + } + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (cvs_casecmp (dp->d_name, fname) == 0) + { + if (found_name != NULL) + error (1, 0, "%s is ambiguous; could mean %s or %s", + fname, dp->d_name, found_name); + found_name = xstrdup (dp->d_name); + } + } + if (errno != 0) + error (1, errno, "cannot read directory %s", dir); + closedir (dirp); + + if (found_name == NULL) + { + *fp = NULL; + retval = ENOENT; + } + else + { + char *p; + + /* Copy the found name back into DIR. We are assuming that + found_name is the same length as fname, which is true as + long as the above code is just ignoring case and not other + aspects of filename syntax. */ + p = dir + strlen (dir); + *p++ = '/'; + strcpy (p, found_name); + *fp = fopen (dir, mode); + if (*fp == NULL) + retval = errno; + else + retval = 0; + } + + if (pathp == NULL) + free (dir); + else if (retval != 0) + free (dir); + else + *pathp = dir; + free (found_name); + out: + return retval; +} +#endif /* SERVER_SUPPORT */ diff --git a/gnu/dist/cvs/src/find_names.c b/gnu/dist/cvs/src/find_names.c new file mode 100644 index 00000000000..6fb927bf6a9 --- /dev/null +++ b/gnu/dist/cvs/src/find_names.c @@ -0,0 +1,425 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Find Names + * + * Finds all the pertinent file names, both from the administration and from the + * repository + * + * Find Dirs + * + * Finds all pertinent sub-directories of the checked out instantiation and the + * repository (and optionally the attic) + */ + +#include "cvs.h" + +static int find_dirs PROTO((char *dir, List * list, int checkadm, + List *entries)); +static int find_rcs PROTO((char *dir, List * list)); +static int add_subdir_proc PROTO((Node *, void *)); +static int register_subdir_proc PROTO((Node *, void *)); + +static List *filelist; + +/* + * add the key from entry on entries list to the files list + */ +static int add_entries_proc PROTO((Node *, void *)); +static int +add_entries_proc (node, closure) + Node *node; + void *closure; +{ + Entnode *entnode; + Node *fnode; + + entnode = (Entnode *) node->data; + if (entnode->type != ENT_FILE) + return (0); + + fnode = getnode (); + fnode->type = FILES; + fnode->key = xstrdup (node->key); + if (addnode (filelist, fnode) != 0) + freenode (fnode); + return (0); +} + +/* Find files in the repository and/or working directory. On error, + may either print a nonfatal error and return NULL, or just give + a fatal error. On success, return non-NULL (even if it is an empty + list). */ + +List * +Find_Names (repository, which, aflag, optentries) + char *repository; + int which; + int aflag; + List **optentries; +{ + List *entries; + List *files; + + /* make a list for the files */ + files = filelist = getlist (); + + /* look at entries (if necessary) */ + if (which & W_LOCAL) + { + /* parse the entries file (if it exists) */ + entries = Entries_Open (aflag, NULL); + if (entries != NULL) + { + /* walk the entries file adding elements to the files list */ + (void) walklist (entries, add_entries_proc, NULL); + + /* if our caller wanted the entries list, return it; else free it */ + if (optentries != NULL) + *optentries = entries; + else + Entries_Close (entries); + } + } + + if ((which & W_REPOS) && repository && !isreadable (CVSADM_ENTSTAT)) + { + /* search the repository */ + if (find_rcs (repository, files) != 0) + { + error (0, errno, "cannot open directory %s", repository); + goto error_exit; + } + + /* search the attic too */ + if (which & W_ATTIC) + { + char *dir; + dir = xmalloc (strlen (repository) + sizeof (CVSATTIC) + 10); + (void) sprintf (dir, "%s/%s", repository, CVSATTIC); + if (find_rcs (dir, files) != 0 + && !existence_error (errno)) + /* For now keep this a fatal error, seems less useful + for access control than the case above. */ + error (1, errno, "cannot open directory %s", dir); + free (dir); + } + } + + /* sort the list into alphabetical order and return it */ + sortlist (files, fsortcmp); + return (files); + error_exit: + dellist (&files); + return NULL; +} + +/* + * Add an entry from the subdirs list to the directories list. This + * is called via walklist. + */ + +static int +add_subdir_proc (p, closure) + Node *p; + void *closure; +{ + List *dirlist = (List *) closure; + Entnode *entnode; + Node *dnode; + + entnode = (Entnode *) p->data; + if (entnode->type != ENT_SUBDIR) + return 0; + + dnode = getnode (); + dnode->type = DIRS; + dnode->key = xstrdup (entnode->user); + if (addnode (dirlist, dnode) != 0) + freenode (dnode); + return 0; +} + +/* + * Register a subdirectory. This is called via walklist. + */ + +/*ARGSUSED*/ +static int +register_subdir_proc (p, closure) + Node *p; + void *closure; +{ + List *entries = (List *) closure; + + Subdir_Register (entries, (char *) NULL, p->key); + return 0; +} + +/* + * create a list of directories to traverse from the current directory + */ +List * +Find_Directories (repository, which, entries) + char *repository; + int which; + List *entries; +{ + List *dirlist; + + /* make a list for the directories */ + dirlist = getlist (); + + /* find the local ones */ + if (which & W_LOCAL) + { + List *tmpentries; + struct stickydirtag *sdtp; + + /* Look through the Entries file. */ + + if (entries != NULL) + tmpentries = entries; + else if (isfile (CVSADM_ENT)) + tmpentries = Entries_Open (0, NULL); + else + tmpentries = NULL; + + if (tmpentries != NULL) + sdtp = (struct stickydirtag *) tmpentries->list->data; + + /* If we do have an entries list, then if sdtp is NULL, or if + sdtp->subdirs is nonzero, all subdirectory information is + recorded in the entries list. */ + if (tmpentries != NULL && (sdtp == NULL || sdtp->subdirs)) + walklist (tmpentries, add_subdir_proc, (void *) dirlist); + else + { + /* This is an old working directory, in which subdirectory + information is not recorded in the Entries file. Find + the subdirectories the hard way, and, if possible, add + it to the Entries file for next time. */ + + /* FIXME-maybe: find_dirs is bogus for this usage because + it skips CVSATTIC and CVSLCK directories--those names + should be special only in the repository. However, in + the interests of not perturbing this code, we probably + should leave well enough alone unless we want to write + a sanity.sh test case (which would operate by manually + hacking on the CVS/Entries file). */ + + if (find_dirs (".", dirlist, 1, tmpentries) != 0) + error (1, errno, "cannot open current directory"); + if (tmpentries != NULL) + { + if (! list_isempty (dirlist)) + walklist (dirlist, register_subdir_proc, + (void *) tmpentries); + else + Subdirs_Known (tmpentries); + } + } + + if (entries == NULL && tmpentries != NULL) + Entries_Close (tmpentries); + } + + /* look for sub-dirs in the repository */ + if ((which & W_REPOS) && repository) + { + /* search the repository */ + if (find_dirs (repository, dirlist, 0, entries) != 0) + error (1, errno, "cannot open directory %s", repository); + + /* We don't need to look in the attic because directories + never go in the attic. In the future, there hopefully will + be a better mechanism for detecting whether a directory in + the repository is alive or dead; it may or may not involve + moving directories to the attic. */ + } + + /* sort the list into alphabetical order and return it */ + sortlist (dirlist, fsortcmp); + return (dirlist); +} + +/* + * Finds all the ,v files in the argument directory, and adds them to the + * files list. Returns 0 for success and non-zero if the argument directory + * cannot be opened, in which case errno is set to indicate the error. + * In the error case LIST is left in some reasonable state (unchanged, or + * containing the files which were found before the error occurred). + */ +static int +find_rcs (dir, list) + char *dir; + List *list; +{ + Node *p; + struct dirent *dp; + DIR *dirp; + + /* set up to read the dir */ + if ((dirp = CVS_OPENDIR (dir)) == NULL) + return (1); + + /* read the dir, grabbing the ,v files */ + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (CVS_FNMATCH (RCSPAT, dp->d_name, 0) == 0) + { + char *comma; + + comma = strrchr (dp->d_name, ','); /* strip the ,v */ + *comma = '\0'; + p = getnode (); + p->type = FILES; + p->key = xstrdup (dp->d_name); + if (addnode (list, p) != 0) + freenode (p); + } + errno = 0; + } + if (errno != 0) + { + int save_errno = errno; + (void) closedir (dirp); + errno = save_errno; + return 1; + } + (void) closedir (dirp); + return (0); +} + +/* + * Finds all the subdirectories of the argument dir and adds them to + * the specified list. Sub-directories without a CVS administration + * directory are optionally ignored. If ENTRIES is not NULL, all + * files on the list are ignored. Returns 0 for success or 1 on + * error, in which case errno is set to indicate the error. + */ +static int +find_dirs (dir, list, checkadm, entries) + char *dir; + List *list; + int checkadm; + List *entries; +{ + Node *p; + char *tmp = NULL; + size_t tmp_size = 0; + struct dirent *dp; + DIR *dirp; + int skip_emptydir = 0; + + /* First figure out whether we need to skip directories named + Emptydir. Except in the CVSNULLREPOS case, Emptydir is just + a normal directory name. */ + if (isabsolute (dir) + && strncmp (dir, CVSroot_directory, strlen (CVSroot_directory)) == 0 + && ISDIRSEP (dir[strlen (CVSroot_directory)]) + && strcmp (dir + strlen (CVSroot_directory) + 1, CVSROOTADM) == 0) + skip_emptydir = 1; + + /* set up to read the dir */ + if ((dirp = CVS_OPENDIR (dir)) == NULL) + return (1); + + /* read the dir, grabbing sub-dirs */ + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (strcmp (dp->d_name, ".") == 0 || + strcmp (dp->d_name, "..") == 0 || + strcmp (dp->d_name, CVSATTIC) == 0 || + strcmp (dp->d_name, CVSLCK) == 0 || + strcmp (dp->d_name, CVSREP) == 0) + goto do_it_again; + + /* findnode() is going to be significantly faster than stat() + because it involves no system calls. That is why we bother + with the entries argument, and why we check this first. */ + if (entries != NULL && findnode (entries, dp->d_name) != NULL) + goto do_it_again; + + if (skip_emptydir + && strcmp (dp->d_name, CVSNULLREPOS) == 0) + goto do_it_again; + +#ifdef DT_DIR + if (dp->d_type != DT_DIR) + { + if (dp->d_type != DT_UNKNOWN && dp->d_type != DT_LNK) + goto do_it_again; +#endif + /* don't bother stating ,v files */ + if (CVS_FNMATCH (RCSPAT, dp->d_name, 0) == 0) + goto do_it_again; + + expand_string (&tmp, + &tmp_size, + strlen (dir) + strlen (dp->d_name) + 10); + sprintf (tmp, "%s/%s", dir, dp->d_name); + if (!isdir (tmp)) + goto do_it_again; + +#ifdef DT_DIR + } +#endif + + /* check for administration directories (if needed) */ + if (checkadm) + { + /* blow off symbolic links to dirs in local dir */ +#ifdef DT_DIR + if (dp->d_type != DT_DIR) + { + /* we're either unknown or a symlink at this point */ + if (dp->d_type == DT_LNK) + goto do_it_again; +#endif + /* Note that we only get here if we already set tmp + above. */ + if (islink (tmp)) + goto do_it_again; +#ifdef DT_DIR + } +#endif + + /* check for new style */ + expand_string (&tmp, + &tmp_size, + (strlen (dir) + strlen (dp->d_name) + + sizeof (CVSADM) + 10)); + (void) sprintf (tmp, "%s/%s/%s", dir, dp->d_name, CVSADM); + if (!isdir (tmp)) + goto do_it_again; + } + + /* put it in the list */ + p = getnode (); + p->type = DIRS; + p->key = xstrdup (dp->d_name); + if (addnode (list, p) != 0) + freenode (p); + + do_it_again: + errno = 0; + } + if (errno != 0) + { + int save_errno = errno; + (void) closedir (dirp); + errno = save_errno; + return 1; + } + (void) closedir (dirp); + if (tmp != NULL) + free (tmp); + return (0); +} diff --git a/gnu/dist/cvs/src/hardlink.c b/gnu/dist/cvs/src/hardlink.c new file mode 100644 index 00000000000..309d27f078d --- /dev/null +++ b/gnu/dist/cvs/src/hardlink.c @@ -0,0 +1,305 @@ +/* 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, 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. */ + +/* Collect and manage hardlink info associated with a particular file. */ + +#include "cvs.h" +#include "hardlink.h" + +/* The structure currently used to manage hardlink info is a list. + Therefore, most of the functions which manipulate hardlink data + are walklist procedures. This is not a very efficient implementation; + if someone decides to use a real hash table (for instance), then + much of this code can be rewritten to be a little less arcane. + + Each element of `hardlist' represents an inode. It is keyed on the + inode number, and points to a list of files. This is to make it + easy to find out what files are linked to a given file FOO: find + FOO's inode, look it up in hardlist, and retrieve the list of files + associated with that inode. + + Each file node, in turn, is represented by a `hardlink_info' struct, + which includes `status' and `links' fields. The `status' field should + be used by a procedure like commit_fileproc or update_fileproc to + record each file's status; that way, after all file links have been + recorded, CVS can check the linkage of files which are in doubt + (i.e. T_NEEDS_MERGE files). + + TODO: a diagram of an example hardlist would help here. */ + +/* TODO: change this to something with a marginal degree of + efficiency, like maybe a hash table. Yeah. */ + +List *hardlist; /* Record hardlink information for working files */ +char *working_dir; /* The top-level working directory, used for + constructing full pathnames. */ + +/* Return a pointer to FILEPATH's node in the hardlist. This means + looking up its inode, retrieving the list of files linked to that + inode, and then looking up FILE in that list. If the file doesn't + seem to exist, return NULL. */ +Node * +lookup_file_by_inode (filepath) + const char *filepath; +{ + char *inodestr, *file; + struct stat sb; + Node *hp, *p; + + /* Get file's basename, so that we can stat it. */ + file = strrchr (filepath, '/'); + if (file) + ++file; + else + file = (char *) filepath; + + /* inodestr contains the hexadecimal representation of an + inode, so it requires two bytes of text to represent + each byte of the inode number. */ + inodestr = (char *) xmalloc (2*sizeof(ino_t) + 1); + if (stat (file, &sb) < 0) + { + if (existence_error (errno)) + { + /* The file doesn't exist; we may be doing an update on a + file that's been removed. A nonexistent file has no + link information, so return without changing hardlist. */ + free (inodestr); + return NULL; + } + error (1, errno, "cannot stat %s", file); + } + + sprintf (inodestr, "%lx", (unsigned long) sb.st_ino); + + /* Find out if this inode is already in the hardlist, adding + a new entry to the list if not. */ + hp = findnode (hardlist, inodestr); + if (hp == NULL) + { + hp = getnode (); + hp->type = NT_UNKNOWN; + hp->key = inodestr; + hp->data = (char *) getlist(); + hp->delproc = dellist; + (void) addnode (hardlist, hp); + } + else + { + free (inodestr); + } + + p = findnode ((List *) hp->data, filepath); + if (p == NULL) + { + p = getnode(); + p->type = NT_UNKNOWN; + p->key = xstrdup (filepath); + p->data = NULL; + (void) addnode ((List *) hp->data, p); + } + + return p; +} + +/* After a file has been checked out, add a node for it to the hardlist + (if necessary) and mark it as checked out. */ +void +update_hardlink_info (file) + const char *file; +{ + char *path; + Node *n; + struct hardlink_info *hlinfo; + + if (file[0] == '/') + { + path = xstrdup (file); + } + else + { + /* file is a relative pathname; assume it's from the current + working directory. */ + char *dir = xgetwd(); + path = xmalloc (strlen(dir) + strlen(file) + 2); + sprintf (path, "%s/%s", dir, file); + free (dir); + } + + n = lookup_file_by_inode (path); + if (n == NULL) + { + /* Something is *really* wrong if the file doesn't exist here; + update_hardlink_info should be called only when a file has + just been checked out to a working directory. */ + error (1, 0, "lost hardlink info for %s", file); + } + + if (n->data == NULL) + n->data = (char *) xmalloc (sizeof (struct hardlink_info)); + hlinfo = (struct hardlink_info *) n->data; + hlinfo->status = T_UPTODATE; + hlinfo->checked_out = 1; +} + +/* Return a List with all the files known to be linked to FILE in + the working directory. Used by special_file_mismatch, to determine + whether it is safe to merge two files. + + FIXME: What is the memory allocation for the return value? We seem + to sometimes allocate a new list (getlist() call below) and sometimes + return an existing list (where we return n->data). */ +List * +list_linked_files_on_disk (file) + char *file; +{ + char *inodestr, *path; + struct stat sb; + Node *n; + + /* If hardlist is NULL, we have not been doing an operation that + would permit us to know anything about the file's hardlinks + (cvs update, cvs commit, etc). Return an empty list. */ + if (hardlist == NULL) + return getlist(); + + /* Get the full pathname of file (assuming the working directory) */ + if (file[0] == '/') + path = xstrdup (file); + else + { + char *dir = xgetwd(); + path = (char *) xmalloc (strlen(dir) + strlen(file) + 2); + sprintf (path, "%s/%s", dir, file); + free (dir); + } + + /* We do an extra lookup_file here just to make sure that there + is a node for `path' in the hardlist. If that were not so, + comparing the working directory linkage against the repository + linkage for a file would always fail. */ + (void) lookup_file_by_inode (path); + + if (stat (path, &sb) < 0) + error (1, errno, "cannot stat %s", file); + /* inodestr contains the hexadecimal representation of an + inode, so it requires two bytes of text to represent + each byte of the inode number. */ + inodestr = (char *) xmalloc (2*sizeof(ino_t) + 1); + sprintf (inodestr, "%lx", (unsigned long) sb.st_ino); + + /* Make sure the files linked to this inode are sorted. */ + n = findnode (hardlist, inodestr); + sortlist ((List *) n->data, fsortcmp); + + free (inodestr); + return (List *) n->data; +} + +/* Compare the files in the `key' fields of two lists, returning 1 if + the lists are equivalent and 0 otherwise. + + Only the basenames of each file are compared. This is an awful hack + that exists because list_linked_files_on_disk returns full paths + and the `hardlinks' structure of a RCSVers node contains only + basenames. That in turn is a result of the awful hack that only + basenames are stored in the RCS file. If anyone ever solves the + problem of correctly managing cross-directory hardlinks, this + function (along with most functions in this file) must be fixed. */ + +int +compare_linkage_lists (links1, links2) + List *links1; + List *links2; +{ + Node *n1, *n2; + char *p1, *p2; + + sortlist (links1, fsortcmp); + sortlist (links2, fsortcmp); + + n1 = links1->list->next; + n2 = links2->list->next; + + while (n1 != links1->list && n2 != links2->list) + { + /* Get the basenames of both files. */ + p1 = strrchr (n1->key, '/'); + if (p1 == NULL) + p1 = n1->key; + else + ++p1; + + p2 = strrchr (n2->key, '/'); + if (p2 == NULL) + p2 = n2->key; + else + ++p2; + + /* Compare the files' basenames. */ + if (strcmp (p1, p2) != 0) + return 0; + + n1 = n1->next; + n2 = n2->next; + } + + /* At this point we should be at the end of both lists; if not, + one file has more links than the other, and return 1. */ + return (n1 == links1->list && n2 == links2->list); +} + +/* Find a checked-out file in a list of filenames. Used by RCS_checkout + when checking out a new hardlinked file, to decide whether this file + can be linked to any others that already exist. The return value + is not currently used. */ + +int +find_checkedout_proc (node, data) + Node *node; + void *data; +{ + Node **uptodate = (Node **) data; + Node *link; + char *dir = xgetwd(); + char *path; + struct hardlink_info *hlinfo; + + /* If we have already found a file, don't do anything. */ + if (*uptodate != NULL) + return 0; + + /* Look at this file in the hardlist and see whether the checked_out + field is 1, meaning that it has been checked out during this CVS run. */ + path = (char *) + xmalloc (strlen (dir) + strlen (node->key) + 2); + sprintf (path, "%s/%s", dir, node->key); + link = lookup_file_by_inode (path); + free (path); + free (dir); + + if (link == NULL) + { + /* We haven't seen this file -- maybe it hasn't been checked + out yet at all. */ + return 0; + } + + hlinfo = (struct hardlink_info *) link->data; + if (hlinfo->checked_out) + { + /* This file has been checked out recently, so it's safe to + link to it. */ + *uptodate = link; + } + + return 0; +} + diff --git a/gnu/dist/cvs/src/hash.c b/gnu/dist/cvs/src/hash.c new file mode 100644 index 00000000000..3f06c266cde --- /dev/null +++ b/gnu/dist/cvs/src/hash.c @@ -0,0 +1,517 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Polk's hash list manager. So cool. + */ + +#include "cvs.h" +#include <assert.h> + +/* Global caches. The idea is that we maintain a linked list of "free"d + nodes or lists, and get new items from there. It has been suggested + to use an obstack instead, but off the top of my head, I'm not sure + that would gain enough to be worth worrying about. */ +static List *listcache = NULL; +static Node *nodecache = NULL; + +static void freenode_mem PROTO((Node * p)); + +/* hash function */ +static int +hashp (key) + const char *key; +{ + unsigned int h = 0; + unsigned int g; + + assert(key != NULL); + + while (*key != 0) + { + unsigned int c = *key++; + /* The FOLD_FN_CHAR is so that findnode_fn works. */ + h = (h << 4) + FOLD_FN_CHAR (c); + if ((g = h & 0xf0000000) != 0) + h = (h ^ (g >> 24)) ^ g; + } + + return (h % HASHSIZE); +} + +/* + * create a new list (or get an old one from the cache) + */ +List * +getlist () +{ + int i; + List *list; + Node *node; + + if (listcache != NULL) + { + /* get a list from the cache and clear it */ + list = listcache; + listcache = listcache->next; + list->next = (List *) NULL; + for (i = 0; i < HASHSIZE; i++) + list->hasharray[i] = (Node *) NULL; + } + else + { + /* make a new list from scratch */ + list = (List *) xmalloc (sizeof (List)); + memset ((char *) list, 0, sizeof (List)); + node = getnode (); + list->list = node; + node->type = HEADER; + node->next = node->prev = node; + } + return (list); +} + +/* + * free up a list + */ +void +dellist (listp) + List **listp; +{ + int i; + Node *p; + + if (*listp == (List *) NULL) + return; + + p = (*listp)->list; + + /* free each node in the list (except header) */ + while (p->next != p) + delnode (p->next); + + /* free any list-private data, without freeing the actual header */ + freenode_mem (p); + + /* free up the header nodes for hash lists (if any) */ + for (i = 0; i < HASHSIZE; i++) + { + if ((p = (*listp)->hasharray[i]) != (Node *) NULL) + { + /* put the nodes into the cache */ +#ifndef NOCACHE + p->type = NT_UNKNOWN; + p->next = nodecache; + nodecache = p; +#else + /* If NOCACHE is defined we turn off the cache. This can make + it easier to tools to determine where items were allocated + and freed, for tracking down memory leaks and the like. */ + free (p); +#endif + } + } + + /* put it on the cache */ +#ifndef NOCACHE + (*listp)->next = listcache; + listcache = *listp; +#else + free ((*listp)->list); + free (*listp); +#endif + *listp = (List *) NULL; +} + +/* + * get a new list node + */ +Node * +getnode () +{ + Node *p; + + if (nodecache != (Node *) NULL) + { + /* get one from the cache */ + p = nodecache; + nodecache = p->next; + } + else + { + /* make a new one */ + p = (Node *) xmalloc (sizeof (Node)); + } + + /* always make it clean */ + memset ((char *) p, 0, sizeof (Node)); + p->type = NT_UNKNOWN; + + return (p); +} + +/* + * remove a node from it's list (maybe hash list too) and free it + */ +void +delnode (p) + Node *p; +{ + if (p == (Node *) NULL) + return; + + /* take it out of the list */ + p->next->prev = p->prev; + p->prev->next = p->next; + + /* if it was hashed, remove it from there too */ + if (p->hashnext != (Node *) NULL) + { + p->hashnext->hashprev = p->hashprev; + p->hashprev->hashnext = p->hashnext; + } + + /* free up the storage */ + freenode (p); +} + +/* + * free up the storage associated with a node + */ +static void +freenode_mem (p) + Node *p; +{ + if (p->delproc != (void (*) ()) NULL) + p->delproc (p); /* call the specified delproc */ + else + { + if (p->data != NULL) /* otherwise free() it if necessary */ + free (p->data); + } + if (p->key != NULL) /* free the key if necessary */ + free (p->key); + + /* to be safe, re-initialize these */ + p->key = p->data = (char *) NULL; + p->delproc = (void (*) ()) NULL; +} + +/* + * free up the storage associated with a node and recycle it + */ +void +freenode (p) + Node *p; +{ + /* first free the memory */ + freenode_mem (p); + + /* then put it in the cache */ +#ifndef NOCACHE + p->type = NT_UNKNOWN; + p->next = nodecache; + nodecache = p; +#else + free (p); +#endif +} + +/* + * Link item P into list LIST before item MARKER. If P->KEY is non-NULL and + * that key is already in the hash table, return -1 without modifying any + * parameter. + * + * return 0 on success + */ +int +insert_before (list, marker, p) + List *list; + Node *marker; + Node *p; +{ + if (p->key != NULL) /* hash it too? */ + { + int hashval; + Node *q; + + hashval = hashp (p->key); + if (list->hasharray[hashval] == NULL) /* make a header for list? */ + { + q = getnode (); + q->type = HEADER; + list->hasharray[hashval] = q->hashnext = q->hashprev = q; + } + + /* put it into the hash list if it's not already there */ + for (q = list->hasharray[hashval]->hashnext; + q != list->hasharray[hashval]; q = q->hashnext) + { + if (strcmp (p->key, q->key) == 0) + return (-1); + } + q = list->hasharray[hashval]; + p->hashprev = q->hashprev; + p->hashnext = q; + p->hashprev->hashnext = p; + q->hashprev = p; + } + + p->next = marker; + p->prev = marker->prev; + marker->prev->next = p; + marker->prev = p; + + return (0); +} + +/* + * insert item p at end of list "list" (maybe hash it too) if hashing and it + * already exists, return -1 and don't actually put it in the list + * + * return 0 on success + */ +int +addnode (list, p) + List *list; + Node *p; +{ + return insert_before (list, list->list, p); +} + +/* + * Like addnode, but insert p at the front of `list'. This bogosity is + * necessary to preserve last-to-first output order for some RCS functions. + */ +int +addnode_at_front (list, p) + List *list; + Node *p; +{ + return insert_before (list, list->list->next, p); +} + +/* Look up an entry in hash list table and return a pointer to the + node. Return NULL if not found. Abort with a fatal error for + errors. */ +Node * +findnode (list, key) + List *list; + const char *key; +{ + Node *head, *p; + + /* This probably should be "assert (list != NULL)" (or if not we + should document the current behavior), but only if we check all + the callers to see if any are relying on this behavior. */ + if ((list == (List *) NULL)) + return ((Node *) NULL); + + assert (key != NULL); + + head = list->hasharray[hashp (key)]; + if (head == (Node *) NULL) + /* Not found. */ + return ((Node *) NULL); + + for (p = head->hashnext; p != head; p = p->hashnext) + if (strcmp (p->key, key) == 0) + return (p); + return ((Node *) NULL); +} + +/* + * Like findnode, but for a filename. + */ +Node * +findnode_fn (list, key) + List *list; + const char *key; +{ + Node *head, *p; + + /* This probably should be "assert (list != NULL)" (or if not we + should document the current behavior), but only if we check all + the callers to see if any are relying on this behavior. */ + if (list == (List *) NULL) + return ((Node *) NULL); + + assert (key != NULL); + + head = list->hasharray[hashp (key)]; + if (head == (Node *) NULL) + return ((Node *) NULL); + + for (p = head->hashnext; p != head; p = p->hashnext) + if (fncmp (p->key, key) == 0) + return (p); + return ((Node *) NULL); +} + +/* + * walk a list with a specific proc + */ +int +walklist (list, proc, closure) + List *list; + int (*proc) PROTO ((Node *, void *)); + void *closure; +{ + Node *head, *p; + int err = 0; + + if (list == NULL) + return (0); + + head = list->list; + for (p = head->next; p != head; p = p->next) + err += proc (p, closure); + return (err); +} + +int +list_isempty (list) + List *list; +{ + return list == NULL || list->list->next == list->list; +} + +static int (*client_comp) PROTO ((const Node *, const Node *)); +static int qsort_comp PROTO ((const void *, const void *)); + +static int +qsort_comp (elem1, elem2) + const void *elem1; + const void *elem2; +{ + Node **node1 = (Node **) elem1; + Node **node2 = (Node **) elem2; + return client_comp (*node1, *node2); +} + +/* + * sort the elements of a list (in place) + */ +void +sortlist (list, comp) + List *list; + int (*comp) PROTO ((const Node *, const Node *)); +{ + Node *head, *remain, *p, **array; + int i, n; + + /* save the old first element of the list */ + head = list->list; + remain = head->next; + + /* count the number of nodes in the list */ + n = 0; + for (p = remain; p != head; p = p->next) + n++; + + /* allocate an array of nodes and populate it */ + array = (Node **) xmalloc (sizeof(Node *) * n); + i = 0; + for (p = remain; p != head; p = p->next) + array[i++] = p; + + /* sort the array of nodes */ + client_comp = comp; + qsort (array, n, sizeof(Node *), qsort_comp); + + /* rebuild the list from beginning to end */ + head->next = head->prev = head; + for (i = 0; i < n; i++) + { + p = array[i]; + p->next = head; + p->prev = head->prev; + p->prev->next = p; + head->prev = p; + } + + /* release the array of nodes */ + free (array); +} + +/* + * compare two files list node (for sort) + */ +int +fsortcmp (p, q) + const Node *p; + const Node *q; +{ + return (strcmp (p->key, q->key)); +} + +/* Debugging functions. Quite useful to call from within gdb. */ + +static char *nodetypestring PROTO ((Ntype)); + +static char * +nodetypestring (type) + Ntype type; +{ + switch (type) { + case NT_UNKNOWN: return("UNKNOWN"); + case HEADER: return("HEADER"); + case ENTRIES: return("ENTRIES"); + case FILES: return("FILES"); + case LIST: return("LIST"); + case RCSNODE: return("RCSNODE"); + case RCSVERS: return("RCSVERS"); + case DIRS: return("DIRS"); + case UPDATE: return("UPDATE"); + case LOCK: return("LOCK"); + case NDBMNODE: return("NDBMNODE"); + case FILEATTR: return("FILEATTR"); + case VARIABLE: return("VARIABLE"); + case RCSFIELD: return("RCSFIELD"); + } + + return("<trash>"); +} + +static int printnode PROTO ((Node *, void *)); +static int +printnode (node, closure) + Node *node; + void *closure; +{ + if (node == NULL) + { + (void) printf("NULL node.\n"); + return(0); + } + + (void) printf("Node at 0x%p: type = %s, key = 0x%p = \"%s\", data = 0x%p, next = 0x%p, prev = 0x%p\n", + node, nodetypestring(node->type), node->key, node->key, node->data, node->next, node->prev); + + return(0); +} + +/* This is global, not static, so that its name is unique and to avoid + compiler warnings about it not being used. But it is not used by CVS; + it exists so one can call it from a debugger. */ +void printlist PROTO ((List *)); + +void +printlist (list) + List *list; +{ + if (list == NULL) + { + (void) printf("NULL list.\n"); + return; + } + + (void) printf("List at 0x%p: list = 0x%p, HASHSIZE = %d, next = 0x%p\n", + list, list->list, HASHSIZE, list->next); + + (void) walklist(list, printnode, NULL); + + return; +} diff --git a/gnu/dist/cvs/src/history.c b/gnu/dist/cvs/src/history.c new file mode 100644 index 00000000000..8cd31d5bdec --- /dev/null +++ b/gnu/dist/cvs/src/history.c @@ -0,0 +1,1587 @@ +/* + * + * You may distribute under the terms of the GNU General Public License + * as specified in the README file that comes with the CVS 1.0 kit. + * + * **************** History of Users and Module **************** + * + * LOGGING: Append record to "${CVSROOT}/CVSROOTADM/CVSROOTADM_HISTORY". + * + * On For each Tag, Add, Checkout, Commit, Update or Release command, + * one line of text is written to a History log. + * + * X date | user | CurDir | special | rev(s) | argument '\n' + * + * where: [The spaces in the example line above are not in the history file.] + * + * X is a single character showing the type of event: + * T "Tag" cmd. + * O "Checkout" cmd. + * E "Export" cmd. + * F "Release" cmd. + * W "Update" cmd - No User file, Remove from Entries file. + * U "Update" cmd - File was checked out over User file. + * G "Update" cmd - File was merged successfully. + * C "Update" cmd - File was merged and shows overlaps. + * M "Commit" cmd - "Modified" file. + * A "Commit" cmd - "Added" file. + * R "Commit" cmd - "Removed" file. + * + * date is a fixed length 8-char hex representation of a Unix time_t. + * [Starting here, variable fields are delimited by '|' chars.] + * + * user is the username of the person who typed the command. + * + * CurDir The directory where the action occurred. This should be the + * absolute path of the directory which is at the same level as + * the "Repository" field (for W,U,G,C & M,A,R). + * + * Repository For record types [W,U,G,C,M,A,R] this field holds the + * repository read from the administrative data where the + * command was typed. + * T "A" --> New Tag, "D" --> Delete Tag + * Otherwise it is the Tag or Date to modify. + * O,F,E A "" (null field) + * + * rev(s) Revision number or tag. + * T The Tag to apply. + * O,E The Tag or Date, if specified, else "" (null field). + * F "" (null field) + * W The Tag or Date, if specified, else "" (null field). + * U The Revision checked out over the User file. + * G,C The Revision(s) involved in merge. + * M,A,R RCS Revision affected. + * + * argument The module (for [TOEUF]) or file (for [WUGCMAR]) affected. + * + * + *** Report categories: "User" and "Since" modifiers apply to all reports. + * [For "sort" ordering see the "sort_order" routine.] + * + * Extract list of record types + * + * -e, -x [TOEFWUGCMAR] + * + * Extracted records are simply printed, No analysis is performed. + * All "field" modifiers apply. -e chooses all types. + * + * Checked 'O'ut modules + * + * -o, -w + * Checked out modules. 'F' and 'O' records are examined and if + * the last record for a repository/file is an 'O', a line is + * printed. "-w" forces the "working dir" to be used in the + * comparison instead of the repository. + * + * Committed (Modified) files + * + * -c, -l, -w + * All 'M'odified, 'A'dded and 'R'emoved records are examined. + * "Field" modifiers apply. -l forces a sort by file within user + * and shows only the last modifier. -w works as in Checkout. + * + * Warning: Be careful with what you infer from the output of + * "cvs hi -c -l". It means the last time *you* + * changed the file, not the list of files for which + * you were the last changer!!! + * + * Module history for named modules. + * -m module, -l + * + * This is special. If one or more modules are specified, the + * module names are remembered and the files making up the + * modules are remembered. Only records matching exactly those + * files and repositories are shown. Sorting by "module", then + * filename, is implied. If -l ("last modified") is specified, + * then "update" records (types WUCG), tag and release records + * are ignored and the last (by date) "modified" record. + * + * TAG history + * + * -T All Tag records are displayed. + * + *** Modifiers. + * + * Since ... [All records contain a timestamp, so any report + * category can be limited by date.] + * + * -D date - The "date" is parsed into a Unix "time_t" and + * records with an earlier time stamp are ignored. + * -r rev/tag - A "rev" begins with a digit. A "tag" does not. If + * you use this option, every file is searched for the + * indicated rev/tag. + * -t tag - The "tag" is searched for in the history file and no + * record is displayed before the tag is found. An + * error is printed if the tag is never found. + * -b string - Records are printed only back to the last reference + * to the string in the "module", "file" or + * "repository" fields. + * + * Field Selections [Simple comparisons on existing fields. All field + * selections are repeatable.] + * + * -a - All users. + * -u user - If no user is given and '-a' is not given, only + * records for the user typing the command are shown. + * ==> If -a or -u is not specified, just use "self". + * + * -f filematch - Only records in which the "file" field contains the + * string "filematch" are considered. + * + * -p repository - Only records in which the "repository" string is a + * prefix of the "repos" field are considered. + * + * -m modulename - Only records which contain "modulename" in the + * "module" field are considered. + * + * + * EXAMPLES: ("cvs history", "cvs his" or "cvs hi") + * + *** Checked out files for username. (default self, e.g. "dgg") + * cvs hi [equivalent to: "cvs hi -o -u dgg"] + * cvs hi -u user [equivalent to: "cvs hi -o -u user"] + * cvs hi -o [equivalent to: "cvs hi -o -u dgg"] + * + *** Committed (modified) files from the beginning of the file. + * cvs hi -c [-u user] + * + *** Committed (modified) files since Midnight, January 1, 1990: + * cvs hi -c -D 'Jan 1 1990' [-u user] + * + *** Committed (modified) files since tag "TAG" was stored in the history file: + * cvs hi -c -t TAG [-u user] + * + *** Committed (modified) files since tag "TAG" was placed on the files: + * cvs hi -c -r TAG [-u user] + * + *** Who last committed file/repository X? + * cvs hi -c -l -[fp] X + * + *** Modified files since tag/date/file/repos? + * cvs hi -c {-r TAG | -D Date | -b string} + * + *** Tag history + * cvs hi -T + * + *** History of file/repository/module X. + * cvs hi -[fpn] X + * + *** History of user "user". + * cvs hi -e -u user + * + *** Dump (eXtract) specified record types + * cvs hi -x [TOFWUGCMAR] + * + * + * FUTURE: J[Join], I[Import] (Not currently implemented.) + * + */ + +#include "cvs.h" +#include "savecwd.h" + +static struct hrec +{ + char *type; /* Type of record (In history record) */ + char *user; /* Username (In history record) */ + char *dir; /* "Compressed" Working dir (In history record) */ + char *repos; /* (Tag is special.) Repository (In history record) */ + char *rev; /* Revision affected (In history record) */ + char *file; /* Filename (In history record) */ + char *end; /* Ptr into repository to copy at end of workdir */ + char *mod; /* The module within which the file is contained */ + time_t date; /* Calculated from date stored in record */ + int idx; /* Index of record, for "stable" sort. */ +} *hrec_head; + + +static void fill_hrec PROTO((char *line, struct hrec * hr)); +static int accept_hrec PROTO((struct hrec * hr, struct hrec * lr)); +static int select_hrec PROTO((struct hrec * hr)); +static int sort_order PROTO((const PTR l, const PTR r)); +static int within PROTO((char *find, char *string)); +static void expand_modules PROTO((void)); +static void read_hrecs PROTO((char *fname)); +static void report_hrecs PROTO((void)); +static void save_file PROTO((char *dir, char *name, char *module)); +static void save_module PROTO((char *module)); +static void save_user PROTO((char *name)); + +#define ALL_REC_TYPES "TOEFWUCGMAR" +#define USER_INCREMENT 2 +#define FILE_INCREMENT 128 +#define MODULE_INCREMENT 5 +#define HREC_INCREMENT 128 + +static short report_count; + +static short extract; +static short v_checkout; +static short modified; +static short tag_report; +static short module_report; +static short working; +static short last_entry; +static short all_users; + +static short user_sort; +static short repos_sort; +static short file_sort; +static short module_sort; + +static short tz_local; +static time_t tz_seconds_east_of_GMT; +static char *tz_name = "+0000"; + +/* -r, -t, or -b options, malloc'd. These are "" if the option in + question is not specified or is overridden by another option. The + main reason for using "" rather than NULL is historical. Together + with since_date, these are a mutually exclusive set; one overrides the + others. */ +static char *since_rev; +static char *since_tag; +static char *backto; +/* -D option, or 0 if not specified. RCS format. */ +static char * since_date; + +static struct hrec *last_since_tag; +static struct hrec *last_backto; + +/* Record types to look for, malloc'd. Probably could be statically + allocated, but only if we wanted to check for duplicates more than + we do. */ +static char *rec_types; + +static int hrec_count; +static int hrec_max; + +static char **user_list; /* Ptr to array of ptrs to user names */ +static int user_max; /* Number of elements allocated */ +static int user_count; /* Number of elements used */ + +static struct file_list_str +{ + char *l_file; + char *l_module; +} *file_list; /* Ptr to array file name structs */ +static int file_max; /* Number of elements allocated */ +static int file_count; /* Number of elements used */ + +static char **mod_list; /* Ptr to array of ptrs to module names */ +static int mod_max; /* Number of elements allocated */ +static int mod_count; /* Number of elements used */ + +static char *histfile; /* Ptr to the history file name */ + +/* This is pretty unclear. First of all, separating "flags" vs. + "options" (I think the distinction is that "options" take arguments) + is nonstandard, and not something we do elsewhere in CVS. Second of + all, what does "reports" mean? I think it means that you can only + supply one of those options, but "reports" hardly has that meaning in + a self-explanatory way. */ +static const char *const history_usg[] = +{ + "Usage: %s %s [-report] [-flags] [-options args] [files...]\n\n", + " Reports:\n", + " -T Produce report on all TAGs\n", + " -c Committed (Modified) files\n", + " -o Checked out modules\n", + " -m <module> Look for specified module (repeatable)\n", + " -x [TOEFWUCGMAR] Extract by record type\n", + " -e Everything (same as -x, but all record types)\n", + " Flags:\n", + " -a All users (Default is self)\n", + " -l Last modified (committed or modified report)\n", + " -w Working directory must match\n", + " Options:\n", + " -D <date> Since date (Many formats)\n", + " -b <str> Back to record with str in module/file/repos field\n", + " -f <file> Specified file (same as command line) (repeatable)\n", + " -n <modulename> In module (repeatable)\n", + " -p <repos> In repository (repeatable)\n", + " -r <rev/tag> Since rev or tag (looks inside RCS files!)\n", + " -t <tag> Since tag record placed in history file (by anyone).\n", + " -u <user> For user name (repeatable)\n", + " -z <tz> Output for time zone <tz> (e.g. -z -0700)\n", + NULL}; + +/* Sort routine for qsort: + - If a user is selected at all, sort it first. User-within-file is useless. + - If a module was selected explicitly, sort next on module. + - Then sort by file. "File" is "repository/file" unless "working" is set, + then it is "workdir/file". (Revision order should always track date.) + - Always sort timestamp last. +*/ +static int +sort_order (l, r) + const PTR l; + const PTR r; +{ + int i; + const struct hrec *left = (const struct hrec *) l; + const struct hrec *right = (const struct hrec *) r; + + if (user_sort) /* If Sort by username, compare users */ + { + if ((i = strcmp (left->user, right->user)) != 0) + return (i); + } + if (module_sort) /* If sort by modules, compare module names */ + { + if (left->mod && right->mod) + if ((i = strcmp (left->mod, right->mod)) != 0) + return (i); + } + if (repos_sort) /* If sort by repository, compare them. */ + { + if ((i = strcmp (left->repos, right->repos)) != 0) + return (i); + } + if (file_sort) /* If sort by filename, compare files, NOT dirs. */ + { + if ((i = strcmp (left->file, right->file)) != 0) + return (i); + + if (working) + { + if ((i = strcmp (left->dir, right->dir)) != 0) + return (i); + + if ((i = strcmp (left->end, right->end)) != 0) + return (i); + } + } + + /* + * By default, sort by date, time + * XXX: This fails after 2030 when date slides into sign bit + */ + if ((i = ((long) (left->date) - (long) (right->date))) != 0) + return (i); + + /* For matching dates, keep the sort stable by using record index */ + return (left->idx - right->idx); +} + +int +history (argc, argv) + int argc; + char **argv; +{ + int i, c; + char *fname; + + if (argc == -1) + usage (history_usg); + + since_rev = xstrdup (""); + since_tag = xstrdup (""); + backto = xstrdup (""); + rec_types = xstrdup (""); + optind = 0; + while ((c = getopt (argc, argv, "+Tacelow?D:b:f:m:n:p:r:t:u:x:X:z:")) != -1) + { + switch (c) + { + case 'T': /* Tag list */ + report_count++; + tag_report++; + break; + case 'a': /* For all usernames */ + all_users++; + break; + case 'c': + report_count++; + modified = 1; + break; + case 'e': + report_count++; + extract++; + free (rec_types); + rec_types = xstrdup (ALL_REC_TYPES); + break; + case 'l': /* Find Last file record */ + last_entry = 1; + break; + case 'o': + report_count++; + v_checkout = 1; + break; + case 'w': /* Match Working Dir (CurDir) fields */ + working = 1; + break; + case 'X': /* Undocumented debugging flag */ + histfile = optarg; + break; + case 'D': /* Since specified date */ + if (*since_rev || *since_tag || *backto) + { + error (0, 0, "date overriding rev/tag/backto"); + *since_rev = *since_tag = *backto = '\0'; + } + since_date = Make_Date (optarg); + break; + case 'b': /* Since specified file/Repos */ + if (since_date || *since_rev || *since_tag) + { + error (0, 0, "backto overriding date/rev/tag"); + *since_rev = *since_tag = '\0'; + if (since_date != NULL) + free (since_date); + since_date = NULL; + } + free (backto); + backto = xstrdup (optarg); + break; + case 'f': /* For specified file */ + save_file ("", optarg, (char *) NULL); + break; + case 'm': /* Full module report */ + report_count++; + module_report++; + case 'n': /* Look for specified module */ + save_module (optarg); + break; + case 'p': /* For specified directory */ + save_file (optarg, "", (char *) NULL); + break; + case 'r': /* Since specified Tag/Rev */ + if (since_date || *since_tag || *backto) + { + error (0, 0, "rev overriding date/tag/backto"); + *since_tag = *backto = '\0'; + if (since_date != NULL) + free (since_date); + since_date = NULL; + } + free (since_rev); + since_rev = xstrdup (optarg); + break; + case 't': /* Since specified Tag/Rev */ + if (since_date || *since_rev || *backto) + { + error (0, 0, "tag overriding date/marker/file/repos"); + *since_rev = *backto = '\0'; + if (since_date != NULL) + free (since_date); + since_date = NULL; + } + free (since_tag); + since_tag = xstrdup (optarg); + break; + case 'u': /* For specified username */ + save_user (optarg); + break; + case 'x': + report_count++; + extract++; + { + char *cp; + + for (cp = optarg; *cp; cp++) + if (!strchr (ALL_REC_TYPES, *cp)) + error (1, 0, "%c is not a valid report type", *cp); + } + free (rec_types); + rec_types = xstrdup (optarg); + break; + case 'z': + tz_local = + (optarg[0] == 'l' || optarg[0] == 'L') + && (optarg[1] == 't' || optarg[1] == 'T') + && !optarg[2]; + if (tz_local) + tz_name = optarg; + else + { + /* + * Convert a known time with the given timezone to time_t. + * Use the epoch + 23 hours, so timezones east of GMT work. + */ + static char f[] = "1/1/1970 23:00 %s"; + char *buf = xmalloc (sizeof (f) - 2 + strlen (optarg)); + time_t t; + sprintf (buf, f, optarg); + t = get_date (buf, (struct timeb *) NULL); + free (buf); + if (t == (time_t) -1) + error (0, 0, "%s is not a known time zone", optarg); + else + { + /* + * Convert to seconds east of GMT, removing the + * 23-hour offset mentioned above. + */ + tz_seconds_east_of_GMT = (time_t)23 * 60 * 60 - t; + tz_name = optarg; + } + } + break; + case '?': + default: + usage (history_usg); + break; + } + } + argc -= optind; + argv += optind; + for (i = 0; i < argc; i++) + save_file ("", argv[i], (char *) NULL); + + + /* ================ Now analyze the arguments a bit */ + if (!report_count) + v_checkout++; + else if (report_count > 1) + error (1, 0, "Only one report type allowed from: \"-Tcomxe\"."); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + struct file_list_str *f1; + char **mod; + + /* We're the client side. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (tag_report) + send_arg("-T"); + if (all_users) + send_arg("-a"); + if (modified) + send_arg("-c"); + if (last_entry) + send_arg("-l"); + if (v_checkout) + send_arg("-o"); + if (working) + send_arg("-w"); + if (histfile) + send_arg("-X"); + if (since_date) + client_senddate (since_date); + if (backto[0] != '\0') + option_with_arg ("-b", backto); + for (f1 = file_list; f1 < &file_list[file_count]; ++f1) + { + if (f1->l_file[0] == '*') + option_with_arg ("-p", f1->l_file + 1); + else + option_with_arg ("-f", f1->l_file); + } + if (module_report) + send_arg("-m"); + for (mod = mod_list; mod < &mod_list[mod_count]; ++mod) + option_with_arg ("-n", *mod); + if (*since_rev) + option_with_arg ("-r", since_rev); + if (*since_tag) + option_with_arg ("-t", since_tag); + for (mod = user_list; mod < &user_list[user_count]; ++mod) + option_with_arg ("-u", *mod); + if (extract) + option_with_arg ("-x", rec_types); + option_with_arg ("-z", tz_name); + + send_to_server ("history\012", 0); + return get_responses_and_close (); + } +#endif + + if (all_users) + save_user (""); + + if (mod_list) + expand_modules (); + + if (tag_report) + { + if (!strchr (rec_types, 'T')) + { + rec_types = xrealloc (rec_types, strlen (rec_types) + 5); + (void) strcat (rec_types, "T"); + } + } + else if (extract) + { + if (user_list) + user_sort++; + } + else if (modified) + { + free (rec_types); + rec_types = xstrdup ("MAR"); + /* + * If the user has not specified a date oriented flag ("Since"), sort + * by Repository/file before date. Default is "just" date. + */ + if (last_entry + || (!since_date && !*since_rev && !*since_tag && !*backto)) + { + repos_sort++; + file_sort++; + /* + * If we are not looking for last_modified and the user specified + * one or more users to look at, sort by user before filename. + */ + if (!last_entry && user_list) + user_sort++; + } + } + else if (module_report) + { + free (rec_types); + rec_types = xstrdup (last_entry ? "OMAR" : ALL_REC_TYPES); + module_sort++; + repos_sort++; + file_sort++; + working = 0; /* User's workdir doesn't count here */ + } + else + /* Must be "checkout" or default */ + { + free (rec_types); + rec_types = xstrdup ("OF"); + /* See comments in "modified" above */ + if (!last_entry && user_list) + user_sort++; + if (last_entry + || (!since_date && !*since_rev && !*since_tag && !*backto)) + file_sort++; + } + + /* If no users were specified, use self (-a saves a universal ("") user) */ + if (!user_list) + save_user (getcaller ()); + + /* If we're looking back to a Tag value, must consider "Tag" records */ + if (*since_tag && !strchr (rec_types, 'T')) + { + rec_types = xrealloc (rec_types, strlen (rec_types) + 5); + (void) strcat (rec_types, "T"); + } + + if (histfile) + fname = xstrdup (histfile); + else + { + fname = xmalloc (strlen (CVSroot_directory) + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_HISTORY) + 10); + (void) sprintf (fname, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_HISTORY); + } + + read_hrecs (fname); + if(hrec_count>0) + { + qsort ((PTR) hrec_head, hrec_count, + sizeof (struct hrec), sort_order); + } + report_hrecs (); + free (fname); + if (since_date != NULL) + free (since_date); + free (since_rev); + free (since_tag); + free (backto); + free (rec_types); + + return (0); +} + +void +history_write (type, update_dir, revs, name, repository) + int type; + char *update_dir; + char *revs; + char *name; + char *repository; +{ + char *fname; + char *workdir; + char *username = getcaller (); + int fd; + char *line; + char *slash = "", *cp, *cp2, *repos; + int i; + static char *tilde = ""; + static char *PrCurDir = NULL; + + if (logoff) /* History is turned off by cmd line switch */ + return; + fname = xmalloc (strlen (CVSroot_directory) + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_HISTORY) + 10); + (void) sprintf (fname, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_HISTORY); + + /* turn off history logging if the history file does not exist */ + if (!isfile (fname)) + { + logoff = 1; + goto out; + } + + if (trace) + fprintf (stderr, "%s-> fopen(%s,a)\n", + CLIENT_SERVER_STR, fname); + if (noexec) + goto out; + fd = CVS_OPEN (fname, O_WRONLY | O_APPEND | O_CREAT | OPEN_BINARY, 0666); + if (fd < 0) + error (1, errno, "cannot open history file: %s", fname); + + repos = Short_Repository (repository); + + if (!PrCurDir) + { + char *pwdir; + + pwdir = get_homedir (); + PrCurDir = CurDir; + if (pwdir != NULL) + { + /* Assumes neither CurDir nor pwdir ends in '/' */ + i = strlen (pwdir); + if (!strncmp (CurDir, pwdir, i)) + { + PrCurDir += i; /* Point to '/' separator */ + tilde = "~"; + } + else + { + /* Try harder to find a "homedir" */ + struct saved_cwd cwd; + char *homedir; + + if (save_cwd (&cwd)) + error_exit (); + + if ( CVS_CHDIR (pwdir) < 0) + error (1, errno, "can't chdir(%s)", pwdir); + homedir = xgetwd (); + if (homedir == NULL) + error (1, errno, "can't getwd in %s", pwdir); + + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + + i = strlen (homedir); + if (!strncmp (CurDir, homedir, i)) + { + PrCurDir += i; /* Point to '/' separator */ + tilde = "~"; + } + free (homedir); + } + } + } + + if (type == 'T') + { + repos = update_dir; + update_dir = ""; + } + else if (update_dir && *update_dir) + slash = "/"; + else + update_dir = ""; + + workdir = xmalloc (strlen (tilde) + strlen (PrCurDir) + strlen (slash) + + strlen (update_dir) + 10); + (void) sprintf (workdir, "%s%s%s%s", tilde, PrCurDir, slash, update_dir); + + /* + * "workdir" is the directory where the file "name" is. ("^~" == $HOME) + * "repos" is the Repository, relative to $CVSROOT where the RCS file is. + * + * "$workdir/$name" is the working file name. + * "$CVSROOT/$repos/$name,v" is the RCS file in the Repository. + * + * First, note that the history format was intended to save space, not + * to be human readable. + * + * The working file directory ("workdir") and the Repository ("repos") + * usually end with the same one or more directory elements. To avoid + * duplication (and save space), the "workdir" field ends with + * an integer offset into the "repos" field. This offset indicates the + * beginning of the "tail" of "repos", after which all characters are + * duplicates. + * + * In other words, if the "workdir" field has a '*' (a very stupid thing + * to put in a filename) in it, then every thing following the last '*' + * is a hex offset into "repos" of the first character from "repos" to + * append to "workdir" to finish the pathname. + * + * It might be easier to look at an example: + * + * M273b3463|dgg|~/work*9|usr/local/cvs/examples|1.2|loginfo + * + * Indicates that the workdir is really "~/work/cvs/examples", saving + * 10 characters, where "~/work*d" would save 6 characters and mean that + * the workdir is really "~/work/examples". It will mean more on + * directories like: usr/local/gnu/emacs/dist-19.17/lisp/term + * + * "workdir" is always an absolute pathname (~/xxx is an absolute path) + * "repos" is always a relative pathname. So we can assume that we will + * never run into the top of "workdir" -- there will always be a '/' or + * a '~' at the head of "workdir" that is not matched by anything in + * "repos". On the other hand, we *can* run off the top of "repos". + * + * Only "compress" if we save characters. + */ + + if (!repos) + repos = ""; + + cp = workdir + strlen (workdir) - 1; + cp2 = repos + strlen (repos) - 1; + for (i = 0; cp2 >= repos && cp > workdir && *cp == *cp2--; cp--) + i++; + + if (i > 2) + { + i = strlen (repos) - i; + (void) sprintf ((cp + 1), "*%x", i); + } + + if (!revs) + revs = ""; + line = xmalloc (strlen (username) + strlen (workdir) + strlen (repos) + + strlen (revs) + strlen (name) + 100); + sprintf (line, "%c%08lx|%s|%s|%s|%s|%s\n", + type, (long) time ((time_t *) NULL), + username, workdir, repos, revs, name); + + /* Lessen some race conditions on non-Posix-compliant hosts. */ + if (lseek (fd, (off_t) 0, SEEK_END) == -1) + error (1, errno, "cannot seek to end of history file: %s", fname); + + if (write (fd, line, strlen (line)) < 0) + error (1, errno, "cannot write to history file: %s", fname); + free (line); + if (close (fd) != 0) + error (1, errno, "cannot close history file: %s", fname); + free (workdir); + out: + free (fname); +} + +/* + * save_user() adds a user name to the user list to select. Zero-length + * username ("") matches any user. + */ +static void +save_user (name) + char *name; +{ + if (user_count == user_max) + { + user_max += USER_INCREMENT; + user_list = (char **) xrealloc ((char *) user_list, + (int) user_max * sizeof (char *)); + } + user_list[user_count++] = xstrdup (name); +} + +/* + * save_file() adds file name and associated module to the file list to select. + * + * If "dir" is null, store a file name as is. + * If "name" is null, store a directory name with a '*' on the front. + * Else, store concatenated "dir/name". + * + * Later, in the "select" stage: + * - if it starts with '*', it is prefix-matched against the repository. + * - if it has a '/' in it, it is matched against the repository/file. + * - else it is matched against the file name. + */ +static void +save_file (dir, name, module) + char *dir; + char *name; + char *module; +{ + char *cp; + struct file_list_str *fl; + + if (file_count == file_max) + { + file_max += FILE_INCREMENT; + file_list = (struct file_list_str *) xrealloc ((char *) file_list, + file_max * sizeof (*fl)); + } + fl = &file_list[file_count++]; + fl->l_file = cp = xmalloc (strlen (dir) + strlen (name) + 2); + fl->l_module = module; + + if (dir && *dir) + { + if (name && *name) + { + (void) strcpy (cp, dir); + (void) strcat (cp, "/"); + (void) strcat (cp, name); + } + else + { + *cp++ = '*'; + (void) strcpy (cp, dir); + } + } + else + { + if (name && *name) + { + (void) strcpy (cp, name); + } + else + { + error (0, 0, "save_file: null dir and file name"); + } + } +} + +static void +save_module (module) + char *module; +{ + if (mod_count == mod_max) + { + mod_max += MODULE_INCREMENT; + mod_list = (char **) xrealloc ((char *) mod_list, + mod_max * sizeof (char *)); + } + mod_list[mod_count++] = xstrdup (module); +} + +static void +expand_modules () +{ +} + +/* fill_hrec + * + * Take a ptr to 7-part history line, ending with a newline, for example: + * + * M273b3463|dgg|~/work*9|usr/local/cvs/examples|1.2|loginfo + * + * Split it into 7 parts and drop the parts into a "struct hrec". + * Return a pointer to the character following the newline. + * + */ + +#define NEXT_BAR(here) do { while (isspace(*line)) line++; hr->here = line; while ((c = *line++) && c != '|') ; if (!c) return; *(line - 1) = '\0'; } while (0) + +static void +fill_hrec (line, hr) + char *line; + struct hrec *hr; +{ + char *cp; + int c; + int off; + static int idx = 0; + unsigned long date; + + memset ((char *) hr, 0, sizeof (*hr)); + + while (isspace ((unsigned char) *line)) + line++; + + hr->type = line++; + (void) sscanf (line, "%lx", &date); + hr->date = date; + while (*line && strchr ("0123456789abcdefABCDEF", *line)) + line++; + if (*line == '\0') + return; + + line++; + NEXT_BAR (user); + NEXT_BAR (dir); + if ((cp = strrchr (hr->dir, '*')) != NULL) + { + *cp++ = '\0'; + (void) sscanf (cp, "%x", &off); + hr->end = line + off; + } + else + hr->end = line - 1; /* A handy pointer to '\0' */ + NEXT_BAR (repos); + NEXT_BAR (rev); + hr->idx = idx++; + if (strchr ("FOET", *(hr->type))) + hr->mod = line; + + NEXT_BAR (file); +} + + +#ifndef STAT_BLOCKSIZE +#if HAVE_ST_BLKSIZE +#define STAT_BLOCKSIZE(s) (s).st_blksize +#else +#define STAT_BLOCKSIZE(s) (4 * 1024) +#endif +#endif + + +/* read_hrecs's job is to read the history file and fill in all the "hrec" + * (history record) array elements with the ones we need to print. + * + * Logic: + * - Read a block from the file. + * - Walk through the block parsing line into hr records. + * - if the hr isn't used, free its strings, if it is, bump the hrec counter + * - at the end of a block, copy the end of the current block to the start + * of space for the next block, then read in the next block. If we get less + * than the whole block, we're done. + */ +static void +read_hrecs (fname) + char *fname; +{ + unsigned char *cpstart, *cp, *nl; + char *hrline; + int i; + int fd; + struct stat st_buf; + + if ((fd = CVS_OPEN (fname, O_RDONLY | OPEN_BINARY)) < 0) + error (1, errno, "cannot open history file: %s", fname); + + if (fstat (fd, &st_buf) < 0) + error (1, errno, "can't stat history file"); + + if (!(st_buf.st_size)) + error (1, 0, "history file is empty"); + + cpstart = xmalloc (2 * STAT_BLOCKSIZE(st_buf)); + cpstart[0] = '\0'; + cp = cpstart; + + hrec_max = HREC_INCREMENT; + hrec_head = xmalloc (hrec_max * sizeof (struct hrec)); + + for (;;) + { + for (nl = cp; *nl && *nl != '\n'; nl++) + if (!isprint(*nl)) *nl = ' '; + + if (!*nl) + { + if (nl - cp >= STAT_BLOCKSIZE(st_buf)) + { + error(1, 0, "history line too long (> %lu)", + (unsigned long) STAT_BLOCKSIZE(st_buf)); + } + if (nl > cp) + memmove (cpstart, cp, nl - cp); + nl = cpstart + (nl - cp); + cp = cpstart; + i = read (fd, nl, STAT_BLOCKSIZE(st_buf)); + if (i > 0) + { + nl[i] = '\0'; + continue; + } + if (i < 0) + error (1, errno, "error reading history file"); + if (nl == cp) break; + nl[1] = '\0'; + } + *nl = '\0'; + + if (hrec_count == hrec_max) + { + struct hrec *old_head = hrec_head; + + hrec_max += HREC_INCREMENT; + hrec_head = xrealloc ((char *) hrec_head, + hrec_max * sizeof (struct hrec)); + if (last_since_tag) + last_since_tag = hrec_head + (last_since_tag - old_head); + if (last_backto) + last_backto = hrec_head + (last_backto - old_head); + } + + /* fill_hrec dates from when history read the entire + history file in one chunk, and then records were pulled out + by pointing to the various parts of this big chunk. This is + why there are ugly hacks here: I don't want to completely + re-write the whole history stuff right now. */ + + hrline = xstrdup ((char *)cp); + fill_hrec (hrline, &hrec_head[hrec_count]); + if (select_hrec (&hrec_head[hrec_count])) + hrec_count++; + else + free(hrline); + + cp = nl + 1; + } + free (cpstart); + close (fd); + + /* Special selection problem: If "since_tag" is set, we have saved every + * record from the 1st occurrence of "since_tag", when we want to save + * records since the *last* occurrence of "since_tag". So what we have + * to do is bump hrec_head forward and reduce hrec_count accordingly. + */ + if (last_since_tag) + { + hrec_count -= (last_since_tag - hrec_head); + hrec_head = last_since_tag; + } + + /* Much the same thing is necessary for the "backto" option. */ + if (last_backto) + { + hrec_count -= (last_backto - hrec_head); + hrec_head = last_backto; + } +} + +/* Utility program for determining whether "find" is inside "string" */ +static int +within (find, string) + char *find, *string; +{ + int c, len; + + if (!find || !string) + return (0); + + c = *find++; + len = strlen (find); + + while (*string) + { + if (!(string = strchr (string, c))) + return (0); + string++; + if (!strncmp (find, string, len)) + return (1); + } + return (0); +} + +/* The purpose of "select_hrec" is to apply the selection criteria based on + * the command arguments and defaults and return a flag indicating whether + * this record should be remembered for printing. + */ +static int +select_hrec (hr) + struct hrec *hr; +{ + char **cpp, *cp, *cp2; + struct file_list_str *fl; + int count; + + /* "Since" checking: The argument parser guarantees that only one of the + * following four choices is set: + * + * 1. If "since_date" is set, it contains the date specified on the + * command line. hr->date fields earlier than "since_date" are ignored. + * 2. If "since_rev" is set, it contains either an RCS "dotted" revision + * number (which is of limited use) or a symbolic TAG. Each RCS file + * is examined and the date on the specified revision (or the revision + * corresponding to the TAG) in the RCS file (CVSROOT/repos/file) is + * compared against hr->date as in 1. above. + * 3. If "since_tag" is set, matching tag records are saved. The field + * "last_since_tag" is set to the last one of these. Since we don't + * know where the last one will be, all records are saved from the + * first occurrence of the TAG. Later, at the end of "select_hrec" + * records before the last occurrence of "since_tag" are skipped. + * 4. If "backto" is set, all records with a module name or file name + * matching "backto" are saved. In addition, all records with a + * repository field with a *prefix* matching "backto" are saved. + * The field "last_backto" is set to the last one of these. As in + * 3. above, "select_hrec" adjusts to include the last one later on. + */ + if (since_date) + { + char *ourdate = date_from_time_t (hr->date); + count = RCS_datecmp (ourdate, since_date); + free (ourdate); + if (count < 0) + return (0); + } + else if (*since_rev) + { + Vers_TS *vers; + time_t t; + struct file_info finfo; + + memset (&finfo, 0, sizeof finfo); + finfo.file = hr->file; + /* Not used, so don't worry about it. */ + finfo.update_dir = NULL; + finfo.fullname = finfo.file; + finfo.repository = hr->repos; + finfo.entries = NULL; + finfo.rcs = NULL; + + vers = Version_TS (&finfo, (char *) NULL, since_rev, (char *) NULL, + 1, 0); + if (vers->vn_rcs) + { + if ((t = RCS_getrevtime (vers->srcfile, vers->vn_rcs, (char *) 0, 0)) + != (time_t) 0) + { + if (hr->date < t) + { + freevers_ts (&vers); + return (0); + } + } + } + freevers_ts (&vers); + } + else if (*since_tag) + { + if (*(hr->type) == 'T') + { + /* + * A 'T'ag record, the "rev" field holds the tag to be set, + * while the "repos" field holds "D"elete, "A"dd or a rev. + */ + if (within (since_tag, hr->rev)) + { + last_since_tag = hr; + return (1); + } + else + return (0); + } + if (!last_since_tag) + return (0); + } + else if (*backto) + { + if (within (backto, hr->file) || within (backto, hr->mod) || + within (backto, hr->repos)) + last_backto = hr; + else + return (0); + } + + /* User checking: + * + * Run down "user_list", match username ("" matches anything) + * If "" is not there and actual username is not there, return failure. + */ + if (user_list && hr->user) + { + for (cpp = user_list, count = user_count; count; cpp++, count--) + { + if (!**cpp) + break; /* null user == accept */ + if (!strcmp (hr->user, *cpp)) /* found listed user */ + break; + } + if (!count) + return (0); /* Not this user */ + } + + /* Record type checking: + * + * 1. If Record type is not in rec_types field, skip it. + * 2. If mod_list is null, keep everything. Otherwise keep only modules + * on mod_list. + * 3. If neither a 'T', 'F' nor 'O' record, run through "file_list". If + * file_list is null, keep everything. Otherwise, keep only files on + * file_list, matched appropriately. + */ + if (!strchr (rec_types, *(hr->type))) + return (0); + if (!strchr ("TFOE", *(hr->type))) /* Don't bother with "file" if "TFOE" */ + { + if (file_list) /* If file_list is null, accept all */ + { + for (fl = file_list, count = file_count; count; fl++, count--) + { + /* 1. If file_list entry starts with '*', skip the '*' and + * compare it against the repository in the hrec. + * 2. If file_list entry has a '/' in it, compare it against + * the concatenation of the repository and file from hrec. + * 3. Else compare the file_list entry against the hrec file. + */ + char *cmpfile = NULL; + + if (*(cp = fl->l_file) == '*') + { + cp++; + /* if argument to -p is a prefix of repository */ + if (!strncmp (cp, hr->repos, strlen (cp))) + { + hr->mod = fl->l_module; + break; + } + } + else + { + if (strchr (cp, '/')) + { + cmpfile = xmalloc (strlen (hr->repos) + + strlen (hr->file) + + 10); + (void) sprintf (cmpfile, "%s/%s", + hr->repos, hr->file); + cp2 = cmpfile; + } + else + { + cp2 = hr->file; + } + + /* if requested file is found within {repos}/file fields */ + if (within (cp, cp2)) + { + hr->mod = fl->l_module; + break; + } + if (cmpfile != NULL) + free (cmpfile); + } + } + if (!count) + return (0); /* String specified and no match */ + } + } + if (mod_list) + { + for (cpp = mod_list, count = mod_count; count; cpp++, count--) + { + if (hr->mod && !strcmp (hr->mod, *cpp)) /* found module */ + break; + } + if (!count) + return (0); /* Module specified & this record is not one of them. */ + } + + return (1); /* Select this record unless rejected above. */ +} + +/* The "sort_order" routine (when handed to qsort) has arranged for the + * hrecs files to be in the right order for the report. + * + * Most of the "selections" are done in the select_hrec routine, but some + * selections are more easily done after the qsort by "accept_hrec". + */ +static void +report_hrecs () +{ + struct hrec *hr, *lr; + struct tm *tm; + int i, count, ty; + char *cp; + int user_len, file_len, rev_len, mod_len, repos_len; + + if (*since_tag && !last_since_tag) + { + (void) printf ("No tag found: %s\n", since_tag); + return; + } + else if (*backto && !last_backto) + { + (void) printf ("No module, file or repository with: %s\n", backto); + return; + } + else if (hrec_count < 1) + { + (void) printf ("No records selected.\n"); + return; + } + + user_len = file_len = rev_len = mod_len = repos_len = 0; + + /* Run through lists and find maximum field widths */ + hr = lr = hrec_head; + hr++; + for (count = hrec_count; count--; lr = hr, hr++) + { + char *repos; + + if (!count) + hr = NULL; + if (!accept_hrec (lr, hr)) + continue; + + ty = *(lr->type); + repos = xstrdup (lr->repos); + if ((cp = strrchr (repos, '/')) != NULL) + { + if (lr->mod && !strcmp (++cp, lr->mod)) + { + (void) strcpy (cp, "*"); + } + } + if ((i = strlen (lr->user)) > user_len) + user_len = i; + if ((i = strlen (lr->file)) > file_len) + file_len = i; + if (ty != 'T' && (i = strlen (repos)) > repos_len) + repos_len = i; + if (ty != 'T' && (i = strlen (lr->rev)) > rev_len) + rev_len = i; + if (lr->mod && (i = strlen (lr->mod)) > mod_len) + mod_len = i; + free (repos); + } + + /* Walk through hrec array setting "lr" (Last Record) to each element. + * "hr" points to the record following "lr" -- It is NULL in the last + * pass. + * + * There are two sections in the loop below: + * 1. Based on the report type (e.g. extract, checkout, tag, etc.), + * decide whether the record should be printed. + * 2. Based on the record type, format and print the data. + */ + for (lr = hrec_head, hr = (lr + 1); hrec_count--; lr = hr, hr++) + { + char *workdir; + char *repos; + + if (!hrec_count) + hr = NULL; + if (!accept_hrec (lr, hr)) + continue; + + ty = *(lr->type); + if (!tz_local) + { + time_t t = lr->date + tz_seconds_east_of_GMT; + tm = gmtime (&t); + } + else + tm = localtime (&(lr->date)); + + (void) printf ("%c %02d/%02d %02d:%02d %s %-*s", ty, tm->tm_mon + 1, + tm->tm_mday, tm->tm_hour, tm->tm_min, tz_name, + user_len, lr->user); + + workdir = xmalloc (strlen (lr->dir) + strlen (lr->end) + 10); + (void) sprintf (workdir, "%s%s", lr->dir, lr->end); + if ((cp = strrchr (workdir, '/')) != NULL) + { + if (lr->mod && !strcmp (++cp, lr->mod)) + { + (void) strcpy (cp, "*"); + } + } + repos = xmalloc (strlen (lr->repos) + 10); + (void) strcpy (repos, lr->repos); + if ((cp = strrchr (repos, '/')) != NULL) + { + if (lr->mod && !strcmp (++cp, lr->mod)) + { + (void) strcpy (cp, "*"); + } + } + + switch (ty) + { + case 'T': + /* 'T'ag records: repository is a "tag type", rev is the tag */ + (void) printf (" %-*s [%s:%s]", mod_len, lr->mod, lr->rev, + repos); + if (working) + (void) printf (" {%s}", workdir); + break; + case 'F': + case 'E': + case 'O': + if (lr->rev && *(lr->rev)) + (void) printf (" [%s]", lr->rev); + (void) printf (" %-*s =%s%-*s %s", repos_len, repos, lr->mod, + mod_len + 1 - (int) strlen (lr->mod), + "=", workdir); + break; + case 'W': + case 'U': + case 'C': + case 'G': + case 'M': + case 'A': + case 'R': + (void) printf (" %-*s %-*s %-*s =%s= %s", rev_len, lr->rev, + file_len, lr->file, repos_len, repos, + lr->mod ? lr->mod : "", workdir); + break; + default: + (void) printf ("Hey! What is this junk? RecType[0x%2.2x]", ty); + break; + } + (void) putchar ('\n'); + free (workdir); + free (repos); + } +} + +static int +accept_hrec (lr, hr) + struct hrec *hr, *lr; +{ + int ty; + + ty = *(lr->type); + + if (last_since_tag && ty == 'T') + return (1); + + if (v_checkout) + { + if (ty != 'O') + return (0); /* Only interested in 'O' records */ + + /* We want to identify all the states that cause the next record + * ("hr") to be different from the current one ("lr") and only + * print a line at the allowed boundaries. + */ + + if (!hr || /* The last record */ + strcmp (hr->user, lr->user) || /* User has changed */ + strcmp (hr->mod, lr->mod) ||/* Module has changed */ + (working && /* If must match "workdir" */ + (strcmp (hr->dir, lr->dir) || /* and the 1st parts or */ + strcmp (hr->end, lr->end)))) /* the 2nd parts differ */ + + return (1); + } + else if (modified) + { + if (!last_entry || /* Don't want only last rec */ + !hr || /* Last entry is a "last entry" */ + strcmp (hr->repos, lr->repos) || /* Repository has changed */ + strcmp (hr->file, lr->file))/* File has changed */ + return (1); + + if (working) + { /* If must match "workdir" */ + if (strcmp (hr->dir, lr->dir) || /* and the 1st parts or */ + strcmp (hr->end, lr->end)) /* the 2nd parts differ */ + return (1); + } + } + else if (module_report) + { + if (!last_entry || /* Don't want only last rec */ + !hr || /* Last entry is a "last entry" */ + strcmp (hr->mod, lr->mod) ||/* Module has changed */ + strcmp (hr->repos, lr->repos) || /* Repository has changed */ + strcmp (hr->file, lr->file))/* File has changed */ + return (1); + } + else + { + /* "extract" and "tag_report" always print selected records. */ + return (1); + } + + return (0); +} diff --git a/gnu/dist/cvs/src/ignore.c b/gnu/dist/cvs/src/ignore.c new file mode 100644 index 00000000000..5156e30f285 --- /dev/null +++ b/gnu/dist/cvs/src/ignore.c @@ -0,0 +1,500 @@ +/* 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, 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. */ + +/* + * .cvsignore file support contributed by David G. Grubbs <dgg@odi.com> + */ + +#include "cvs.h" +#include "getline.h" + +/* + * Ignore file section. + * + * "!" may be included any time to reset the list (i.e. ignore nothing); + * "*" may be specified to ignore everything. It stays as the first + * element forever, unless a "!" clears it out. + */ + +static char **ign_list; /* List of files to ignore in update + * and import */ +static char **s_ign_list = NULL; +static int ign_count; /* Number of active entries */ +static int s_ign_count = 0; +static int ign_size; /* This many slots available (plus + * one for a NULL) */ +static int ign_hold = -1; /* Index where first "temporary" item + * is held */ + +const char *ign_default = ". .. core RCSLOG tags TAGS RCS SCCS .make.state\ + .nse_depinfo #* .#* cvslog.* ,* CVS CVS.adm .del-* *.a *.olb *.o *.obj\ + *.so *.Z *~ *.old *.elc *.ln *.bak *.BAK *.orig *.rej *.exe _$* *$"; + +#define IGN_GROW 16 /* grow the list by 16 elements at a + * time */ + +/* Nonzero if we have encountered an -I ! directive, which means one should + no longer ask the server about what is in CVSROOTADM_IGNORE. */ +int ign_inhibit_server; + +/* + * To the "ignore list", add the hard-coded default ignored wildcards above, + * the wildcards found in $CVSROOT/CVSROOT/cvsignore, the wildcards found in + * ~/.cvsignore and the wildcards found in the CVSIGNORE environment + * variable. + */ +void +ign_setup () +{ + char *home_dir; + char *tmp; + + ign_inhibit_server = 0; + + /* Start with default list and special case */ + tmp = xstrdup (ign_default); + ign_add (tmp, 0); + free (tmp); + +#ifdef CLIENT_SUPPORT + /* The client handles another way, by (after it does its own ignore file + processing, and only if !ign_inhibit_server), letting the server + know about the files and letting it decide whether to ignore + them based on CVSROOOTADM_IGNORE. */ + if (!client_active) +#endif + { + char *file = xmalloc (strlen (CVSroot_directory) + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_IGNORE) + 10); + /* Then add entries found in repository, if it exists */ + (void) sprintf (file, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_IGNORE); + ign_add_file (file, 0); + free (file); + } + + /* Then add entries found in home dir, (if user has one) and file exists */ + home_dir = get_homedir (); + /* If we can't find a home directory, ignore ~/.cvsignore. This may + make tracking down problems a bit of a pain, but on the other + hand it might be obnoxious to complain when CVS will function + just fine without .cvsignore (and many users won't even know what + .cvsignore is). */ + if (home_dir) + { + char *file = xmalloc (strlen (home_dir) + sizeof (CVSDOTIGNORE) + 10); + (void) sprintf (file, "%s/%s", home_dir, CVSDOTIGNORE); + ign_add_file (file, 0); + free (file); + } + + /* Then add entries found in CVSIGNORE environment variable. */ + ign_add (getenv (IGNORE_ENV), 0); + + /* Later, add ignore entries found in -I arguments */ +} + +/* + * Open a file and read lines, feeding each line to a line parser. Arrange + * for keeping a temporary list of wildcards at the end, if the "hold" + * argument is set. + */ +void +ign_add_file (file, hold) + char *file; + int hold; +{ + FILE *fp; + char *line = NULL; + size_t line_allocated = 0; + + /* restore the saved list (if any) */ + if (s_ign_list != NULL) + { + int i; + + for (i = 0; i < s_ign_count; i++) + ign_list[i] = s_ign_list[i]; + ign_count = s_ign_count; + ign_list[ign_count] = NULL; + + s_ign_count = 0; + free (s_ign_list); + s_ign_list = NULL; + } + + /* is this a temporary ignore file? */ + if (hold) + { + /* re-set if we had already done a temporary file */ + if (ign_hold >= 0) + { + int i; + + for (i = ign_hold; i < ign_count; i++) + free (ign_list[i]); + ign_count = ign_hold; + ign_list[ign_count] = NULL; + } + else + { + ign_hold = ign_count; + } + } + + /* load the file */ + fp = CVS_FOPEN (file, "r"); + if (fp == NULL) + { + if (! existence_error (errno)) + error (0, errno, "cannot open %s", file); + return; + } + while (getline (&line, &line_allocated, fp) >= 0) + ign_add (line, hold); + if (ferror (fp)) + error (0, errno, "cannot read %s", file); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", file); + free (line); +} + +/* Parse a line of space-separated wildcards and add them to the list. */ +void +ign_add (ign, hold) + char *ign; + int hold; +{ + if (!ign || !*ign) + return; + + for (; *ign; ign++) + { + char *mark; + char save; + + /* ignore whitespace before the token */ + if (isspace ((unsigned char) *ign)) + continue; + + /* + * if we find a single character !, we must re-set the ignore list + * (saving it if necessary). We also catch * as a special case in a + * global ignore file as an optimization + */ + if ((!*(ign+1) || isspace ((unsigned char) *(ign+1))) + && (*ign == '!' || *ign == '*')) + { + if (!hold) + { + /* permanently reset the ignore list */ + int i; + + for (i = 0; i < ign_count; i++) + free (ign_list[i]); + ign_count = 0; + ign_list[0] = NULL; + + /* if we are doing a '!', continue; otherwise add the '*' */ + if (*ign == '!') + { + ign_inhibit_server = 1; + continue; + } + } + else if (*ign == '!') + { + /* temporarily reset the ignore list */ + int i; + + if (ign_hold >= 0) + { + for (i = ign_hold; i < ign_count; i++) + free (ign_list[i]); + ign_hold = -1; + } + s_ign_list = (char **) xmalloc (ign_count * sizeof (char *)); + for (i = 0; i < ign_count; i++) + s_ign_list[i] = ign_list[i]; + s_ign_count = ign_count; + ign_count = 0; + ign_list[0] = NULL; + continue; + } + } + + /* If we have used up all the space, add some more */ + if (ign_count >= ign_size) + { + ign_size += IGN_GROW; + ign_list = (char **) xrealloc ((char *) ign_list, + (ign_size + 1) * sizeof (char *)); + } + + /* find the end of this token */ + for (mark = ign; *mark && !isspace ((unsigned char) *mark); mark++) + /* do nothing */ ; + + save = *mark; + *mark = '\0'; + + ign_list[ign_count++] = xstrdup (ign); + ign_list[ign_count] = NULL; + + *mark = save; + if (save) + ign = mark; + else + ign = mark - 1; + } +} + +/* Set to 1 if filenames should be matched in a case-insensitive + fashion. Note that, contrary to the name and placement in ignore.c, + this is no longer just for ignore patterns. */ +int ign_case; + +/* Return 1 if the given filename should be ignored by update or import. */ +int +ign_name (name) + char *name; +{ + char **cpp = ign_list; + + if (cpp == NULL) + return (0); + + if (ign_case) + { + /* We do a case-insensitive match by calling fnmatch on copies of + the pattern and the name which have been converted to + lowercase. FIXME: would be much cleaner to just unify this + with the other case-insensitive fnmatch stuff (FOLD_FN_CHAR + in lib/fnmatch.c; os2_fnmatch in emx/system.c). */ + char *name_lower; + char *pat_lower; + char *p; + + name_lower = xstrdup (name); + for (p = name_lower; *p != '\0'; ++p) + *p = tolower (*p); + while (*cpp) + { + pat_lower = xstrdup (*cpp++); + for (p = pat_lower; *p != '\0'; ++p) + *p = tolower (*p); + if (CVS_FNMATCH (pat_lower, name_lower, 0) == 0) + goto matched; + free (pat_lower); + } + free (name_lower); + return 0; + matched: + free (name_lower); + free (pat_lower); + return 1; + } + else + { + while (*cpp) + if (CVS_FNMATCH (*cpp++, name, 0) == 0) + return 1; + return 0; + } +} + +/* FIXME: This list of dirs to ignore stuff seems not to be used. + Really? send_dirent_proc and update_dirent_proc both call + ignore_directory and do_module calls ign_dir_add. No doubt could + use some documentation/testsuite work. */ + +static char **dir_ign_list = NULL; +static int dir_ign_max = 0; +static int dir_ign_current = 0; + +/* Add a directory to list of dirs to ignore. */ +void +ign_dir_add (name) + char *name; +{ + /* Make sure we've got the space for the entry. */ + if (dir_ign_current <= dir_ign_max) + { + dir_ign_max += IGN_GROW; + dir_ign_list = + (char **) xrealloc (dir_ign_list, + (dir_ign_max + 1) * sizeof (char *)); + } + + dir_ign_list[dir_ign_current] = name; + + dir_ign_current += 1 ; +} + + +/* Return nonzero if NAME is part of the list of directories to ignore. */ + +int +ignore_directory (name) + char *name; +{ + int i; + + if (!dir_ign_list) + return 0; + + i = dir_ign_current; + while (i--) + { + if (strncmp (name, dir_ign_list[i], strlen (dir_ign_list[i])) == 0) + return 1; + } + + return 0; +} + +/* + * Process the current directory, looking for files not in ILIST and + * not on the global ignore list for this directory. If we find one, + * call PROC passing it the name of the file and the update dir. + * ENTRIES is the entries list, which is used to identify known + * directories. ENTRIES may be NULL, in which case we assume that any + * directory with a CVS administration directory is known. + */ +void +ignore_files (ilist, entries, update_dir, proc) + List *ilist; + List *entries; + char *update_dir; + Ignore_proc proc; +{ + int subdirs; + DIR *dirp; + struct dirent *dp; + struct stat sb; + char *file; + char *xdir; + + /* Set SUBDIRS if we have subdirectory information in ENTRIES. */ + if (entries == NULL) + subdirs = 0; + else + { + struct stickydirtag *sdtp; + + sdtp = (struct stickydirtag *) entries->list->data; + subdirs = sdtp == NULL || sdtp->subdirs; + } + + /* we get called with update_dir set to "." sometimes... strip it */ + if (strcmp (update_dir, ".") == 0) + xdir = ""; + else + xdir = update_dir; + + dirp = CVS_OPENDIR ("."); + if (dirp == NULL) + { + error (0, errno, "cannot open current directory"); + return; + } + + ign_add_file (CVSDOTIGNORE, 1); + wrap_add_file (CVSDOTWRAPPER, 1); + + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + file = dp->d_name; + if (strcmp (file, ".") == 0 || strcmp (file, "..") == 0) + continue; + if (findnode_fn (ilist, file) != NULL) + continue; + if (subdirs) + { + Node *node; + + node = findnode_fn (entries, file); + if (node != NULL + && ((Entnode *) node->data)->type == ENT_SUBDIR) + { + char *p; + int dir; + + /* For consistency with past behaviour, we only ignore + this directory if there is a CVS subdirectory. + This will normally be the case, but the user may + have messed up the working directory somehow. */ + p = xmalloc (strlen (file) + sizeof CVSADM + 10); + sprintf (p, "%s/%s", file, CVSADM); + dir = isdir (p); + free (p); + if (dir) + continue; + } + } + + /* We could be ignoring FIFOs and other files which are neither + regular files nor directories here. */ + if (ign_name (file)) + continue; + + if ( +#ifdef DT_DIR + dp->d_type != DT_UNKNOWN || +#endif + lstat(file, &sb) != -1) + { + + if ( +#ifdef DT_DIR + dp->d_type == DT_DIR + || (dp->d_type == DT_UNKNOWN && S_ISDIR (sb.st_mode)) +#else + S_ISDIR (sb.st_mode) +#endif + ) + { + if (! subdirs) + { + char *temp; + + temp = xmalloc (strlen (file) + sizeof (CVSADM) + 10); + (void) sprintf (temp, "%s/%s", file, CVSADM); + if (isdir (temp)) + { + free (temp); + continue; + } + free (temp); + } + } +#ifdef S_ISLNK + else if ( +#ifdef DT_DIR + dp->d_type == DT_LNK + || (dp->d_type == DT_UNKNOWN && S_ISLNK(sb.st_mode)) +#else + S_ISLNK (sb.st_mode) +#endif + ) + { + continue; + } +#endif + } + + (*proc) (file, xdir); + errno = 0; + } + if (errno != 0) + error (0, errno, "error reading current directory"); + (void) closedir (dirp); +} diff --git a/gnu/dist/cvs/src/import.c b/gnu/dist/cvs/src/import.c new file mode 100644 index 00000000000..37e5abdffa4 --- /dev/null +++ b/gnu/dist/cvs/src/import.c @@ -0,0 +1,1596 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * "import" checks in the vendor release located in the current directory into + * the CVS source repository. The CVS vendor branch support is utilized. + * + * At least three arguments are expected to follow the options: + * repository Where the source belongs relative to the CVSROOT + * VendorTag Vendor's major tag + * VendorReleTag Tag for this particular release + * + * Additional arguments specify more Vendor Release Tags. + */ + +#include "cvs.h" +#include "savecwd.h" +#include <assert.h> + +static char *get_comment PROTO((char *user)); +static int add_rev PROTO((char *message, RCSNode *rcs, char *vfile, + char *vers)); +static int add_tags PROTO((RCSNode *rcs, char *vfile, char *vtag, int targc, + char *targv[])); +static int import_descend PROTO((char *message, char *vtag, int targc, char *targv[])); +static int import_descend_dir PROTO((char *message, char *dir, char *vtag, + int targc, char *targv[])); +static int process_import_file PROTO((char *message, char *vfile, char *vtag, + int targc, char *targv[])); +static int update_rcs_file PROTO((char *message, char *vfile, char *vtag, int targc, + char *targv[], int inattic)); +static void add_log PROTO((int ch, char *fname)); + +static int repos_len; +static char *vhead; +static char *vbranch; +static FILE *logfp; +static char *repository; +static int conflicts; +static int use_file_modtime; +static char *keyword_opt = NULL; + +static const char *const import_usage[] = +{ + "Usage: %s %s [-d] [-k subst] [-I ign] [-m msg] [-b branch]\n", + " [-W spec] repository vendor-tag release-tags...\n", + "\t-d\tUse the file's modification time as the time of import.\n", + "\t-k sub\tSet default RCS keyword substitution mode.\n", + "\t-I ign\tMore files to ignore (! to reset).\n", + "\t-b bra\tVendor branch id.\n", + "\t-m msg\tLog message.\n", + "\t-W spec\tWrappers specification line.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +import (argc, argv) + int argc; + char **argv; +{ + char *message = NULL; + char *tmpfile; + char *cp; + int i, c, msglen, err; + List *ulist; + Node *p; + struct logfile_info *li; + + if (argc == -1) + usage (import_usage); + + ign_setup (); + wrap_setup (); + + vbranch = xstrdup (CVSBRANCH); + optind = 0; + while ((c = getopt (argc, argv, "+Qqdb:m:I:k:W:")) != -1) + { + switch (c) + { + case 'Q': + case 'q': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'd': +#ifdef SERVER_SUPPORT + if (server_active) + { + /* CVS 1.10 and older clients will send this, but it + doesn't do any good. So tell the user we can't + cope, rather than silently losing. */ + error (0, 0, + "warning: not setting the time of import from the file"); + error (0, 0, "due to client limitations"); + } +#endif + use_file_modtime = 1; + break; + case 'b': + free (vbranch); + vbranch = xstrdup (optarg); + break; + case 'm': +#ifdef FORCE_USE_EDITOR + use_editor = 1; +#else + use_editor = 0; +#endif + message = xstrdup(optarg); + break; + case 'I': + ign_add (optarg, 0); + break; + case 'k': + /* RCS_check_kflag returns strings of the form -kxx. We + only use it for validation, so we can free the value + as soon as it is returned. */ + free (RCS_check_kflag (optarg)); + keyword_opt = optarg; + break; + case 'W': + wrap_add (optarg, 0); + break; + case '?': + default: + usage (import_usage); + break; + } + } + argc -= optind; + argv += optind; + if (argc < 3) + usage (import_usage); + +#ifdef SERVER_SUPPORT + /* This is for handling the Checkin-time request. It might seem a + bit odd to enable the use_file_modtime code even in the case + where Checkin-time was not sent for a particular file. The + effect is that we use the time of upload, rather than the time + when we call RCS_checkin. Since those times are both during + CVS's run, that seems OK, and it is easier to implement than + putting the "was Checkin-time sent" flag in CVS/Entries or some + such place. */ + + if (server_active) + use_file_modtime = 1; +#endif + + for (i = 1; i < argc; i++) /* check the tags for validity */ + { + int j; + + RCS_check_tag (argv[i]); + for (j = 1; j < i; j++) + if (strcmp (argv[j], argv[i]) == 0) + error (1, 0, "tag `%s' was specified more than once", argv[i]); + } + + /* XXX - this should be a module, not just a pathname */ + if (! isabsolute (argv[0]) + && pathname_levels (argv[0]) == 0) + { + if (CVSroot_directory == NULL) + { + error (0, 0, "missing CVSROOT environment variable\n"); + error (1, 0, "Set it or specify the '-d' option to %s.", + program_name); + } + repository = xmalloc (strlen (CVSroot_directory) + strlen (argv[0]) + + 10); + (void) sprintf (repository, "%s/%s", CVSroot_directory, argv[0]); + repos_len = strlen (CVSroot_directory); + } + else + { + /* It is somewhere between a security hole and "unexpected" to + let the client start mucking around outside the cvsroot + (wouldn't get the right CVSROOT configuration, &c). */ + error (1, 0, "directory %s not relative within the repository", + argv[0]); + } + + /* + * Consistency checks on the specified vendor branch. It must be + * composed of only numbers and dots ('.'). Also, for now we only + * support branching to a single level, so the specified vendor branch + * must only have two dots in it (like "1.1.1"). + */ + for (cp = vbranch; *cp != '\0'; cp++) + if (!isdigit ((unsigned char) *cp) && *cp != '.') + error (1, 0, "%s is not a numeric branch", vbranch); + if (numdots (vbranch) != 2) + error (1, 0, "Only branches with two dots are supported: %s", vbranch); + vhead = xstrdup (vbranch); + cp = strrchr (vhead, '.'); + *cp = '\0'; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* For rationale behind calling start_server before do_editor, see + commit.c */ + start_server (); + } +#endif + + if (use_editor) + { + do_editor ((char *) NULL, &message, repository, + (List *) NULL); + } + do_verify (message, repository); + msglen = message == NULL ? 0 : strlen (message); + if (msglen == 0 || message[msglen - 1] != '\n') + { + char *nm = xmalloc (msglen + 2); + *nm = '\0'; + if (message != NULL) + { + (void) strcpy (nm, message); + free (message); + } + (void) strcat (nm + msglen, "\n"); + message = nm; + } + +#ifdef CLIENT_SUPPORT + if (client_active) + { + int err; + + if (vbranch[0] != '\0') + option_with_arg ("-b", vbranch); + if (message) + option_with_arg ("-m", message); + if (keyword_opt != NULL) + option_with_arg ("-k", keyword_opt); + /* The only ignore processing which takes place on the server side + is the CVSROOT/cvsignore file. But if the user specified -I !, + the documented behavior is to not process said file. */ + if (ign_inhibit_server) + { + send_arg ("-I"); + send_arg ("!"); + } + wrap_send (); + + { + int i; + for (i = 0; i < argc; ++i) + send_arg (argv[i]); + } + + logfp = stdin; + client_import_setup (repository); + err = import_descend (message, argv[1], argc - 2, argv + 2); + client_import_done (); + send_to_server ("import\012", 0); + err += get_responses_and_close (); + return err; + } +#endif + + /* + * Make all newly created directories writable. Should really use a more + * sophisticated security mechanism here. + */ + (void) umask (cvsumask); + make_directories (repository); + + /* Create the logfile that will be logged upon completion */ + tmpfile = cvs_temp_name (); + if ((logfp = CVS_FOPEN (tmpfile, "w+")) == NULL) + error (1, errno, "cannot create temporary file `%s'", tmpfile); + /* On systems where we can unlink an open file, do so, so it will go + away no matter how we exit. FIXME-maybe: Should be checking for + errors but I'm not sure which error(s) we get if we are on a system + where one can't unlink open files. */ + (void) CVS_UNLINK (tmpfile); + (void) fprintf (logfp, "\nVendor Tag:\t%s\n", argv[1]); + (void) fprintf (logfp, "Release Tags:\t"); + for (i = 2; i < argc; i++) + (void) fprintf (logfp, "%s\n\t\t", argv[i]); + (void) fprintf (logfp, "\n"); + + /* Just Do It. */ + err = import_descend (message, argv[1], argc - 2, argv + 2); + if (conflicts) + { + if (!really_quiet) + { + char buf[20]; + char *buf2; + + cvs_output_tagged ("+importmergecmd", NULL); + cvs_output_tagged ("newline", NULL); + sprintf (buf, "%d", conflicts); + cvs_output_tagged ("conflicts", buf); + cvs_output_tagged ("text", " conflicts created by this import."); + cvs_output_tagged ("newline", NULL); + cvs_output_tagged ("text", + "Use the following command to help the merge:"); + cvs_output_tagged ("newline", NULL); + cvs_output_tagged ("newline", NULL); + cvs_output_tagged ("text", "\t"); + cvs_output_tagged ("text", program_name); + if (CVSroot_cmdline != NULL) + { + cvs_output_tagged ("text", " -d "); + cvs_output_tagged ("text", CVSroot_cmdline); + } + cvs_output_tagged ("text", " checkout -j"); + buf2 = xmalloc (strlen (argv[1]) + 20); + sprintf (buf2, "%s:yesterday", argv[1]); + cvs_output_tagged ("mergetag1", buf2); + free (buf2); + cvs_output_tagged ("text", " -j"); + cvs_output_tagged ("mergetag2", argv[1]); + cvs_output_tagged ("text", " "); + cvs_output_tagged ("repository", argv[0]); + cvs_output_tagged ("newline", NULL); + cvs_output_tagged ("newline", NULL); + cvs_output_tagged ("-importmergecmd", NULL); + } + + /* FIXME: I'm not sure whether we need to put this information + into the loginfo. If we do, then note that it does not + report any required -d option. There is no particularly + clean way to tell the server about the -d option used by + the client. */ + (void) fprintf (logfp, "\n%d conflicts created by this import.\n", + conflicts); + (void) fprintf (logfp, + "Use the following command to help the merge:\n\n"); + (void) fprintf (logfp, "\t%s checkout ", program_name); + (void) fprintf (logfp, "-j%s:yesterday -j%s %s\n\n", + argv[1], argv[1], argv[0]); + } + else + { + if (!really_quiet) + cvs_output ("\nNo conflicts created by this import\n\n", 0); + (void) fprintf (logfp, "\nNo conflicts created by this import\n\n"); + } + + /* + * Write out the logfile and clean up. + */ + ulist = getlist (); + p = getnode (); + p->type = UPDATE; + p->delproc = update_delproc; + p->key = xstrdup ("- Imported sources"); + li = (struct logfile_info *) xmalloc (sizeof (struct logfile_info)); + li->type = T_TITLE; + li->tag = xstrdup (vbranch); + li->rev_old = li->rev_new = NULL; + p->data = (char *) li; + (void) addnode (ulist, p); + Update_Logfile (repository, message, logfp, ulist); + dellist (&ulist); + if (fclose (logfp) < 0) + error (0, errno, "error closing %s", tmpfile); + + /* Make sure the temporary file goes away, even on systems that don't let + you delete a file that's in use. */ + if (CVS_UNLINK (tmpfile) < 0 && !existence_error (errno)) + error (0, errno, "cannot remove %s", tmpfile); + free (tmpfile); + + if (message) + free (message); + free (repository); + free (vbranch); + free (vhead); + + return (err); +} + +/* Process all the files in ".", then descend into other directories. + Returns 0 for success, or >0 on error (in which case a message + will have been printed). */ +static int +import_descend (message, vtag, targc, targv) + char *message; + char *vtag; + int targc; + char *targv[]; +{ + DIR *dirp; + struct dirent *dp; + int err = 0; + List *dirlist = NULL; + + /* first, load up any per-directory ignore lists */ + ign_add_file (CVSDOTIGNORE, 1); + wrap_add_file (CVSDOTWRAPPER, 1); + + if ((dirp = CVS_OPENDIR (".")) == NULL) + { + error (0, errno, "cannot open directory"); + err++; + } + else + { + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (strcmp (dp->d_name, ".") == 0 || strcmp (dp->d_name, "..") == 0) + goto one_more_time_boys; +#ifdef SERVER_SUPPORT + /* CVS directories are created in the temp directory by + server.c because it doesn't special-case import. So + don't print a message about them, regardless of -I!. */ + if (server_active && strcmp (dp->d_name, CVSADM) == 0) + goto one_more_time_boys; +#endif + if (ign_name (dp->d_name)) + { + add_log ('I', dp->d_name); + goto one_more_time_boys; + } + + if ( +#ifdef DT_DIR + (dp->d_type == DT_DIR + || (dp->d_type == DT_UNKNOWN && isdir (dp->d_name))) +#else + isdir (dp->d_name) +#endif + && !wrap_name_has (dp->d_name, WRAP_TOCVS) + ) + { + Node *n; + + if (dirlist == NULL) + dirlist = getlist(); + + n = getnode(); + n->key = xstrdup (dp->d_name); + addnode(dirlist, n); + } + else if ( +#ifdef DT_DIR + dp->d_type == DT_LNK + || (dp->d_type == DT_UNKNOWN && islink (dp->d_name)) +#else + islink (dp->d_name) +#endif + ) + { + add_log ('L', dp->d_name); + err++; + } + else + { +#ifdef CLIENT_SUPPORT + if (client_active) + err += client_process_import_file (message, dp->d_name, + vtag, targc, targv, + repository, + keyword_opt != NULL && + keyword_opt[0] == 'b', + use_file_modtime); + else +#endif + err += process_import_file (message, dp->d_name, + vtag, targc, targv); + } + one_more_time_boys: + errno = 0; + } + if (errno != 0) + { + error (0, errno, "cannot read directory"); + ++err; + } + (void) closedir (dirp); + } + + if (dirlist != NULL) + { + Node *head, *p; + + head = dirlist->list; + for (p = head->next; p != head; p = p->next) + { + err += import_descend_dir (message, p->key, vtag, targc, targv); + } + + dellist(&dirlist); + } + + return (err); +} + +/* + * Process the argument import file. + */ +static int +process_import_file (message, vfile, vtag, targc, targv) + char *message; + char *vfile; + char *vtag; + int targc; + char *targv[]; +{ + char *rcs; + int inattic = 0; + + rcs = xmalloc (strlen (repository) + strlen (vfile) + sizeof (RCSEXT) + + 5); + (void) sprintf (rcs, "%s/%s%s", repository, vfile, RCSEXT); + if (!isfile (rcs)) + { + char *attic_name; + + attic_name = xmalloc (strlen (repository) + strlen (vfile) + + sizeof (CVSATTIC) + sizeof (RCSEXT) + 10); + (void) sprintf (attic_name, "%s/%s/%s%s", repository, CVSATTIC, + vfile, RCSEXT); + if (!isfile (attic_name)) + { + int retval; + char *free_opt = NULL; + char *our_opt = keyword_opt; + + free (attic_name); + /* + * A new import source file; it doesn't exist as a ,v within the + * repository nor in the Attic -- create it anew. + */ + add_log ('N', vfile); + +#ifdef SERVER_SUPPORT + /* The most reliable information on whether the file is binary + is what the client told us. That is because if the client had + the wrong idea about binaryness, it corrupted the file, so + we might as well believe the client. */ + if (server_active) + { + Node *node; + List *entries; + + /* Reading all the entries for each file is fairly silly, and + probably slow. But I am too lazy at the moment to do + anything else. */ + entries = Entries_Open (0, NULL); + node = findnode_fn (entries, vfile); + if (node != NULL) + { + Entnode *entdata = (Entnode *) node->data; + if (entdata->type == ENT_FILE) + { + assert (entdata->options[0] == '-' + && entdata->options[1] == 'k'); + our_opt = xstrdup (entdata->options + 2); + free_opt = our_opt; + } + } + Entries_Close (entries); + } +#endif + + retval = add_rcs_file (message, rcs, vfile, vhead, our_opt, + vbranch, vtag, targc, targv, + NULL, 0, logfp); + if (free_opt != NULL) + free (free_opt); + free (rcs); + return retval; + } + free (attic_name); + inattic = 1; + } + + free (rcs); + /* + * an rcs file exists. have to do things the official, slow, way. + */ + return (update_rcs_file (message, vfile, vtag, targc, targv, inattic)); +} + +/* + * The RCS file exists; update it by adding the new import file to the + * (possibly already existing) vendor branch. + */ +static int +update_rcs_file (message, vfile, vtag, targc, targv, inattic) + char *message; + char *vfile; + char *vtag; + int targc; + char *targv[]; + int inattic; +{ + Vers_TS *vers; + int letter; + char *tocvsPath; + struct file_info finfo; + + memset (&finfo, 0, sizeof finfo); + finfo.file = vfile; + /* Not used, so don't worry about it. */ + finfo.update_dir = NULL; + finfo.fullname = finfo.file; + finfo.repository = repository; + finfo.entries = NULL; + finfo.rcs = NULL; + vers = Version_TS (&finfo, (char *) NULL, vbranch, (char *) NULL, + 1, 0); + if (vers->vn_rcs != NULL + && !RCS_isdead(vers->srcfile, vers->vn_rcs)) + { + int different; + + /* + * The rcs file does have a revision on the vendor branch. Compare + * this revision with the import file; if they match exactly, there + * is no need to install the new import file as a new revision to the + * branch. Just tag the revision with the new import tags. + * + * This is to try to cut down the number of "C" conflict messages for + * locally modified import source files. + */ + tocvsPath = wrap_tocvs_process_file (vfile); + /* FIXME: Why don't we pass tocvsPath to RCS_cmp_file if it is + not NULL? */ + different = RCS_cmp_file (vers->srcfile, vers->vn_rcs, "-ko", vfile); + if (tocvsPath) + if (unlink_file_dir (tocvsPath) < 0) + error (0, errno, "cannot remove %s", tocvsPath); + + if (!different) + { + int retval = 0; + + /* + * The two files are identical. Just update the tags, print the + * "U", signifying that the file has changed, but needs no + * attention, and we're done. + */ + if (add_tags (vers->srcfile, vfile, vtag, targc, targv)) + retval = 1; + add_log ('U', vfile); + freevers_ts (&vers); + return (retval); + } + } + + /* We may have failed to parse the RCS file; check just in case */ + if (vers->srcfile == NULL || + add_rev (message, vers->srcfile, vfile, vers->vn_rcs) || + add_tags (vers->srcfile, vfile, vtag, targc, targv)) + { + freevers_ts (&vers); + return (1); + } + + if (vers->srcfile->branch == NULL || inattic || + strcmp (vers->srcfile->branch, vbranch) != 0) + { + conflicts++; + letter = 'C'; + } + else + letter = 'U'; + add_log (letter, vfile); + + freevers_ts (&vers); + return (0); +} + +/* + * Add the revision to the vendor branch + */ +static int +add_rev (message, rcs, vfile, vers) + char *message; + RCSNode *rcs; + char *vfile; + char *vers; +{ + int locked, status, ierrno; + char *tocvsPath; + + if (noexec) + return (0); + + locked = 0; + if (vers != NULL) + { + /* Before RCS_lock existed, we were directing stdout, as well as + stderr, from the RCS command, to DEVNULL. I wouldn't guess that + was necessary, but I don't know for sure. */ + /* Earlier versions of this function printed a `fork failed' error + when RCS_lock returned an error code. That's not appropriate + now that RCS_lock is librarified, but should the error text be + preserved? */ + if (RCS_lock (rcs, vbranch, 1) != 0) + return 1; + locked = 1; + RCS_rewrite (rcs, NULL, NULL); + } + tocvsPath = wrap_tocvs_process_file (vfile); + + status = RCS_checkin (rcs, tocvsPath == NULL ? vfile : tocvsPath, + message, vbranch, + (RCS_FLAGS_QUIET | RCS_FLAGS_KEEPFILE + | (use_file_modtime ? RCS_FLAGS_MODTIME : 0))); + ierrno = errno; + + if ((tocvsPath != NULL) && (unlink_file_dir (tocvsPath) < 0)) + error (0, errno, "cannot remove %s", tocvsPath); + + if (status) + { + if (!noexec) + { + fperrmsg (logfp, 0, status == -1 ? ierrno : 0, + "ERROR: Check-in of %s failed", rcs->path); + error (0, status == -1 ? ierrno : 0, + "ERROR: Check-in of %s failed", rcs->path); + } + if (locked) + { + (void) RCS_unlock(rcs, vbranch, 0); + RCS_rewrite (rcs, NULL, NULL); + } + return (1); + } + return (0); +} + +/* + * Add the vendor branch tag and all the specified import release tags to the + * RCS file. The vendor branch tag goes on the branch root (1.1.1) while the + * vendor release tags go on the newly added leaf of the branch (1.1.1.1, + * 1.1.1.2, ...). + */ +static int +add_tags (rcs, vfile, vtag, targc, targv) + RCSNode *rcs; + char *vfile; + char *vtag; + int targc; + char *targv[]; +{ + int i, ierrno; + Vers_TS *vers; + int retcode = 0; + struct file_info finfo; + + if (noexec) + return (0); + + if ((retcode = RCS_settag(rcs, vtag, vbranch)) != 0) + { + ierrno = errno; + fperrmsg (logfp, 0, retcode == -1 ? ierrno : 0, + "ERROR: Failed to set tag %s in %s", vtag, rcs->path); + error (0, retcode == -1 ? ierrno : 0, + "ERROR: Failed to set tag %s in %s", vtag, rcs->path); + return (1); + } + RCS_rewrite (rcs, NULL, NULL); + + memset (&finfo, 0, sizeof finfo); + finfo.file = vfile; + /* Not used, so don't worry about it. */ + finfo.update_dir = NULL; + finfo.fullname = finfo.file; + finfo.repository = repository; + finfo.entries = NULL; + finfo.rcs = NULL; + vers = Version_TS (&finfo, NULL, vtag, NULL, 1, 0); + for (i = 0; i < targc; i++) + { + if ((retcode = RCS_settag (rcs, targv[i], vers->vn_rcs)) == 0) + RCS_rewrite (rcs, NULL, NULL); + else + { + ierrno = errno; + fperrmsg (logfp, 0, retcode == -1 ? ierrno : 0, + "WARNING: Couldn't add tag %s to %s", targv[i], + rcs->path); + error (0, retcode == -1 ? ierrno : 0, + "WARNING: Couldn't add tag %s to %s", targv[i], + rcs->path); + } + } + freevers_ts (&vers); + return (0); +} + +/* + * Stolen from rcs/src/rcsfnms.c, and adapted/extended. + */ +struct compair +{ + char *suffix, *comlead; +}; + +static const struct compair comtable[] = +{ + +/* + * comtable pairs each filename suffix with a comment leader. The comment + * leader is placed before each line generated by the $Log keyword. This + * table is used to guess the proper comment leader from the working file's + * suffix during initial ci (see InitAdmin()). Comment leaders are needed for + * languages without multiline comments; for others they are optional. + * + * I believe that the comment leader is unused if you are using RCS 5.7, which + * decides what leader to use based on the text surrounding the $Log keyword + * rather than a specified comment leader. + */ + {"a", "-- "}, /* Ada */ + {"ada", "-- "}, + {"adb", "-- "}, + {"asm", ";; "}, /* assembler (MS-DOS) */ + {"ads", "-- "}, /* Ada */ + {"bas", "' "}, /* Visual Basic code */ + {"bat", ":: "}, /* batch (MS-DOS) */ + {"body", "-- "}, /* Ada */ + {"c", " * "}, /* C */ + {"c++", "// "}, /* C++ in all its infinite guises */ + {"cc", "// "}, + {"cpp", "// "}, + {"cxx", "// "}, + {"m", "// "}, /* Objective-C */ + {"cl", ";;; "}, /* Common Lisp */ + {"cmd", ":: "}, /* command (OS/2) */ + {"cmf", "c "}, /* CM Fortran */ + {"cs", " * "}, /* C* */ + {"csh", "# "}, /* shell */ + {"dlg", " * "}, /* MS Windows dialog file */ + {"e", "# "}, /* efl */ + {"epsf", "% "}, /* encapsulated postscript */ + {"epsi", "% "}, /* encapsulated postscript */ + {"el", "; "}, /* Emacs Lisp */ + {"f", "c "}, /* Fortran */ + {"for", "c "}, + {"frm", "' "}, /* Visual Basic form */ + {"h", " * "}, /* C-header */ + {"hh", "// "}, /* C++ header */ + {"hpp", "// "}, + {"hxx", "// "}, + {"in", "# "}, /* for Makefile.in */ + {"l", " * "}, /* lex (conflict between lex and + * franzlisp) */ + {"mac", ";; "}, /* macro (DEC-10, MS-DOS, PDP-11, + * VMS, etc) */ + {"mak", "# "}, /* makefile, e.g. Visual C++ */ + {"me", ".\\\" "}, /* me-macros t/nroff */ + {"ml", "; "}, /* mocklisp */ + {"mm", ".\\\" "}, /* mm-macros t/nroff */ + {"ms", ".\\\" "}, /* ms-macros t/nroff */ + {"man", ".\\\" "}, /* man-macros t/nroff */ + {"1", ".\\\" "}, /* feeble attempt at man pages... */ + {"2", ".\\\" "}, + {"3", ".\\\" "}, + {"4", ".\\\" "}, + {"5", ".\\\" "}, + {"6", ".\\\" "}, + {"7", ".\\\" "}, + {"8", ".\\\" "}, + {"9", ".\\\" "}, + {"p", " * "}, /* pascal */ + {"pas", " * "}, + {"pl", "# "}, /* perl (conflict with Prolog) */ + {"ps", "% "}, /* postscript */ + {"psw", "% "}, /* postscript wrap */ + {"pswm", "% "}, /* postscript wrap */ + {"r", "# "}, /* ratfor */ + {"rc", " * "}, /* Microsoft Windows resource file */ + {"red", "% "}, /* psl/rlisp */ +#ifdef sparc + {"s", "! "}, /* assembler */ +#endif +#ifdef mc68000 + {"s", "| "}, /* assembler */ +#endif +#ifdef pdp11 + {"s", "/ "}, /* assembler */ +#endif +#ifdef vax + {"s", "# "}, /* assembler */ +#endif +#ifdef __ksr__ + {"s", "# "}, /* assembler */ + {"S", "# "}, /* Macro assembler */ +#endif + {"sh", "# "}, /* shell */ + {"sl", "% "}, /* psl */ + {"spec", "-- "}, /* Ada */ + {"tex", "% "}, /* tex */ + {"y", " * "}, /* yacc */ + {"ye", " * "}, /* yacc-efl */ + {"yr", " * "}, /* yacc-ratfor */ + {"", "# "}, /* default for empty suffix */ + {NULL, "# "} /* default for unknown suffix; */ +/* must always be last */ +}; + +static char * +get_comment (user) + char *user; +{ + char *cp, *suffix; + char *suffix_path; + int i; + char *retval; + + suffix_path = xmalloc (strlen (user) + 5); + cp = strrchr (user, '.'); + if (cp != NULL) + { + cp++; + + /* + * Convert to lower-case, since we are not concerned about the + * case-ness of the suffix. + */ + (void) strcpy (suffix_path, cp); + for (cp = suffix_path; *cp; cp++) + if (isupper ((unsigned char) *cp)) + *cp = tolower (*cp); + suffix = suffix_path; + } + else + suffix = ""; /* will use the default */ + for (i = 0;; i++) + { + if (comtable[i].suffix == NULL) + { + /* Default. Note we'll always hit this case before we + ever return NULL. */ + retval = comtable[i].comlead; + break; + } + if (strcmp (suffix, comtable[i].suffix) == 0) + { + retval = comtable[i].comlead; + break; + } + } + free (suffix_path); + return retval; +} + +/* Create a new RCS file from scratch. + + This probably should be moved to rcs.c now that it is called from + places outside import.c. + + Return value is 0 for success, or nonzero for failure (in which + case an error message will have already been printed). */ +int +add_rcs_file (message, rcs, user, add_vhead, key_opt, + add_vbranch, vtag, targc, targv, + desctext, desclen, add_logfp) + /* Log message for the addition. Not used if add_vhead == NULL. */ + char *message; + /* Filename of the RCS file to create. */ + char *rcs; + /* Filename of the file to serve as the contents of the initial + revision. Even if add_vhead is NULL, we use this to determine + the modes to give the new RCS file. */ + char *user; + + /* Revision number of head that we are adding. Normally 1.1 but + could be another revision as long as ADD_VBRANCH is a branch + from it. If NULL, then just add an empty file without any + revisions (similar to the one created by "rcs -i"). */ + char *add_vhead; + + /* Keyword expansion mode, e.g., "b" for binary. NULL means the + default behavior. */ + char *key_opt; + + /* Vendor branch to import to, or NULL if none. If non-NULL, then + vtag should also be non-NULL. */ + char *add_vbranch; + char *vtag; + int targc; + char *targv[]; + + /* If non-NULL, description for the file. If NULL, the description + will be empty. */ + char *desctext; + size_t desclen; + + /* Write errors to here as well as via error (), or NULL if we should + use only error (). */ + FILE *add_logfp; +{ + FILE *fprcs, *fpuser; + struct stat sb; + struct tm *ftm; + time_t now; + char altdate1[MAXDATELEN]; + char *author; + int i, ierrno, err = 0; + mode_t mode; + char *tocvsPath; + char *userfile; + char *local_opt = key_opt; + char *free_opt = NULL; + mode_t file_type; + + if (noexec) + return (0); + + /* Note that as the code stands now, the -k option overrides any + settings in wrappers (whether CVSROOT/cvswrappers, -W, or + whatever). Some have suggested this should be the other way + around. As far as I know the documentation doesn't say one way + or the other. Before making a change of this sort, should think + about what is best, document it (in cvs.texinfo and NEWS), &c. */ + + if (local_opt == NULL) + { + if (wrap_name_has (user, WRAP_RCSOPTION)) + { + local_opt = free_opt = wrap_rcsoption (user, 0); + } + } + + tocvsPath = wrap_tocvs_process_file (user); + userfile = (tocvsPath == NULL ? user : tocvsPath); + + /* Opening in text mode is probably never the right thing for the + server (because the protocol encodes text files in a fashion + which does not depend on what the client or server OS is, as + documented in cvsclient.texi), but as long as the server just + runs on unix it is a moot point. */ + + /* If PreservePermissions is set, then make sure that the file + is a plain file before trying to open it. Longstanding (although + often unpopular) CVS behavior has been to follow symlinks, so we + maintain that behavior if PreservePermissions is not on. + + NOTE: this error message used to be `cannot fstat', but is now + `cannot lstat'. I don't see a way around this, since we must + stat the file before opening it. -twp */ + + if (CVS_LSTAT (userfile, &sb) < 0) + error (1, errno, "cannot lstat %s", userfile); + file_type = sb.st_mode & S_IFMT; + + fpuser = NULL; + if (!preserve_perms || file_type == S_IFREG) + { + fpuser = CVS_FOPEN (userfile, + ((local_opt != NULL && strcmp (local_opt, "b") == 0) + ? "rb" + : "r") + ); + if (fpuser == NULL) + { + /* not fatal, continue import */ + if (add_logfp != NULL) + fperrmsg (add_logfp, 0, errno, + "ERROR: cannot read file %s", userfile); + error (0, errno, "ERROR: cannot read file %s", userfile); + goto read_error; + } + } + + fprcs = CVS_FOPEN (rcs, "w+b"); + if (fprcs == NULL) + { + ierrno = errno; + goto write_error_noclose; + } + + /* + * putadmin() + */ + if (add_vhead != NULL) + { + if (fprintf (fprcs, "head %s;\012", add_vhead) < 0) + goto write_error; + } + else + { + if (fprintf (fprcs, "head ;\012") < 0) + goto write_error; + } + + if (add_vbranch != NULL) + { + if (fprintf (fprcs, "branch %s;\012", add_vbranch) < 0) + goto write_error; + } + if (fprintf (fprcs, "access ;\012") < 0 || + fprintf (fprcs, "symbols ") < 0) + { + goto write_error; + } + + for (i = targc - 1; i >= 0; i--) + { + /* RCS writes the symbols backwards */ + assert (add_vbranch != NULL); + if (fprintf (fprcs, "%s:%s.1 ", targv[i], add_vbranch) < 0) + goto write_error; + } + + if (add_vbranch != NULL) + { + if (fprintf (fprcs, "%s:%s", vtag, add_vbranch) < 0) + goto write_error; + } + if (fprintf (fprcs, ";\012") < 0) + goto write_error; + + if (fprintf (fprcs, "locks ; strict;\012") < 0 || + /* XXX - make sure @@ processing works in the RCS file */ + fprintf (fprcs, "comment @%s@;\012", get_comment (user)) < 0) + { + goto write_error; + } + + if (local_opt != NULL) + { + if (fprintf (fprcs, "expand @%s@;\012", local_opt) < 0) + { + goto write_error; + } + } + + if (fprintf (fprcs, "\012") < 0) + goto write_error; + + /* Write the revision(s), with the date and author and so on + (that is "delta" rather than "deltatext" from rcsfile(5)). */ + if (add_vhead != NULL) + { + if (use_file_modtime) + now = sb.st_mtime; + else + (void) time (&now); + ftm = gmtime (&now); + (void) sprintf (altdate1, DATEFORM, + ftm->tm_year + (ftm->tm_year < 100 ? 0 : 1900), + ftm->tm_mon + 1, ftm->tm_mday, ftm->tm_hour, + ftm->tm_min, ftm->tm_sec); + author = getcaller (); + + if (fprintf (fprcs, "\012%s\012", add_vhead) < 0 || + fprintf (fprcs, "date %s; author %s; state Exp;\012", + altdate1, author) < 0) + goto write_error; + + if (fprintf (fprcs, "branches") < 0) + goto write_error; + if (add_vbranch != NULL) + { + if (fprintf (fprcs, " %s.1", add_vbranch) < 0) + goto write_error; + } + if (fprintf (fprcs, ";\012") < 0) + goto write_error; + + if (fprintf (fprcs, "next ;\012") < 0) + goto write_error; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* Store initial permissions if necessary. */ + if (preserve_perms) + { + if (file_type == S_IFLNK) + { + char *link = xreadlink (userfile); + if (fprintf (fprcs, "symlink\t@") < 0 || + expand_at_signs (link, strlen (link), fprcs) < 0 || + fprintf (fprcs, "@;\012") < 0) + goto write_error; + free (link); + } + else + { + if (fprintf (fprcs, "owner\t%u;\012", sb.st_uid) < 0) + goto write_error; + if (fprintf (fprcs, "group\t%u;\012", sb.st_gid) < 0) + goto write_error; + if (fprintf (fprcs, "permissions\t%o;\012", + sb.st_mode & 07777) < 0) + goto write_error; + switch (file_type) + { + case S_IFREG: break; + case S_IFCHR: + case S_IFBLK: + if (fprintf (fprcs, "special\t%s %lu;\012", + (file_type == S_IFCHR + ? "character" + : "block"), + (unsigned long) sb.st_rdev) < 0) + goto write_error; + break; + default: + error (0, 0, + "can't import %s: unknown kind of special file", + userfile); + } + } + } +#endif + + if (add_vbranch != NULL) + { + if (fprintf (fprcs, "\012%s.1\012", add_vbranch) < 0 || + fprintf (fprcs, "date %s; author %s; state Exp;\012", + altdate1, author) < 0 || + fprintf (fprcs, "branches ;\012") < 0 || + fprintf (fprcs, "next ;\012") < 0) + goto write_error; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* Store initial permissions if necessary. */ + if (preserve_perms) + { + if (file_type == S_IFLNK) + { + char *link = xreadlink (userfile); + if (fprintf (fprcs, "symlink\t@") < 0 || + expand_at_signs (link, strlen (link), fprcs) < 0 || + fprintf (fprcs, "@;\012") < 0) + goto write_error; + free (link); + } + else + { + if (fprintf (fprcs, "owner\t%u;\012", sb.st_uid) < 0 || + fprintf (fprcs, "group\t%u;\012", sb.st_gid) < 0 || + fprintf (fprcs, "permissions\t%o;\012", + sb.st_mode & 07777) < 0) + goto write_error; + + switch (file_type) + { + case S_IFREG: break; + case S_IFCHR: + case S_IFBLK: + if (fprintf (fprcs, "special\t%s %lu;\012", + (file_type == S_IFCHR + ? "character" + : "block"), + (unsigned long) sb.st_rdev) < 0) + goto write_error; + break; + default: + error (0, 0, + "cannot import %s: special file of unknown type", + userfile); + } + } + } +#endif + + if (fprintf (fprcs, "\012") < 0) + goto write_error; + } + } + + /* Now write the description (possibly empty). */ + if (fprintf (fprcs, "\012desc\012") < 0 || + fprintf (fprcs, "@") < 0) + goto write_error; + if (desctext != NULL) + { + /* The use of off_t not size_t for the second argument is very + strange, since we are dealing with something which definitely + fits in memory. */ + if (expand_at_signs (desctext, (off_t) desclen, fprcs) < 0) + goto write_error; + } + if (fprintf (fprcs, "@\012\012\012") < 0) + goto write_error; + + /* Now write the log messages and contents for the revision(s) (that + is, "deltatext" rather than "delta" from rcsfile(5)). */ + if (add_vhead != NULL) + { + if (fprintf (fprcs, "\012%s\012", add_vhead) < 0 || + fprintf (fprcs, "log\012@") < 0) + goto write_error; + if (add_vbranch != NULL) + { + /* We are going to put the log message in the revision on the + branch. So putting it here too seems kind of redundant, I + guess (and that is what CVS has always done, anyway). */ + if (fprintf (fprcs, "Initial revision\012") < 0) + goto write_error; + } + else + { + if (expand_at_signs (message, (off_t) strlen (message), fprcs) < 0) + goto write_error; + } + if (fprintf (fprcs, "@\012") < 0 || + fprintf (fprcs, "text\012@") < 0) + { + goto write_error; + } + + /* Now copy over the contents of the file, expanding at signs. + If preserve_perms is set, do this only for regular files. */ + if (!preserve_perms || file_type == S_IFREG) + { + char buf[8192]; + unsigned int len; + + while (1) + { + len = fread (buf, 1, sizeof buf, fpuser); + if (len == 0) + { + if (ferror (fpuser)) + error (1, errno, "cannot read file %s for copying", + user); + break; + } + if (expand_at_signs (buf, len, fprcs) < 0) + goto write_error; + } + } + if (fprintf (fprcs, "@\012\012") < 0) + goto write_error; + if (add_vbranch != NULL) + { + if (fprintf (fprcs, "\012%s.1\012", add_vbranch) < 0 || + fprintf (fprcs, "log\012@") < 0 || + expand_at_signs (message, + (off_t) strlen (message), fprcs) < 0 || + fprintf (fprcs, "@\012text\012") < 0 || + fprintf (fprcs, "@@\012") < 0) + goto write_error; + } + } + + if (fclose (fprcs) == EOF) + { + ierrno = errno; + goto write_error_noclose; + } + /* Close fpuser only if we opened it to begin with. */ + if (fpuser != NULL) + { + if (fclose (fpuser) < 0) + error (0, errno, "cannot close %s", user); + } + + /* + * Fix the modes on the RCS files. The user modes of the original + * user file are propagated to the group and other modes as allowed + * by the repository umask, except that all write permissions are + * turned off. + */ + mode = (sb.st_mode | + (sb.st_mode & S_IRWXU) >> 3 | + (sb.st_mode & S_IRWXU) >> 6) & + ~cvsumask & + ~(S_IWRITE | S_IWGRP | S_IWOTH); + if (chmod (rcs, mode) < 0) + { + ierrno = errno; + if (add_logfp != NULL) + fperrmsg (add_logfp, 0, ierrno, + "WARNING: cannot change mode of file %s", rcs); + error (0, ierrno, "WARNING: cannot change mode of file %s", rcs); + err++; + } + if (tocvsPath) + if (unlink_file_dir (tocvsPath) < 0) + error (0, errno, "cannot remove %s", tocvsPath); + if (free_opt != NULL) + free (free_opt); + return (err); + +write_error: + ierrno = errno; + if (fclose (fprcs) < 0) + error (0, errno, "cannot close %s", rcs); +write_error_noclose: + if (fclose (fpuser) < 0) + error (0, errno, "cannot close %s", user); + if (add_logfp != NULL) + fperrmsg (add_logfp, 0, ierrno, "ERROR: cannot write file %s", rcs); + error (0, ierrno, "ERROR: cannot write file %s", rcs); + if (ierrno == ENOSPC) + { + if (CVS_UNLINK (rcs) < 0) + error (0, errno, "cannot remove %s", rcs); + if (add_logfp != NULL) + fperrmsg (add_logfp, 0, 0, "ERROR: out of space - aborting"); + error (1, 0, "ERROR: out of space - aborting"); + } +read_error: + if (tocvsPath) + if (unlink_file_dir (tocvsPath) < 0) + error (0, errno, "cannot remove %s", tocvsPath); + + if (free_opt != NULL) + free (free_opt); + + return (err + 1); +} + +/* + * Write SIZE bytes at BUF to FP, expanding @ signs into double @ + * signs. If an error occurs, return a negative value and set errno + * to indicate the error. If not, return a nonnegative value. + */ +int +expand_at_signs (buf, size, fp) + char *buf; + off_t size; + FILE *fp; +{ + register char *cp, *next; + + cp = buf; + while ((next = memchr (cp, '@', size)) != NULL) + { + int len; + + ++next; + len = next - cp; + if (fwrite (cp, 1, len, fp) != len) + return EOF; + if (putc ('@', fp) == EOF) + return EOF; + cp = next; + size -= len; + } + + if (fwrite (cp, 1, size, fp) != size) + return EOF; + + return 1; +} + +/* + * Write an update message to (potentially) the screen and the log file. + */ +static void +add_log (ch, fname) + int ch; + char *fname; +{ + if (!really_quiet) /* write to terminal */ + { + char buf[2]; + buf[0] = ch; + buf[1] = ' '; + cvs_output (buf, 2); + if (repos_len) + { + cvs_output (repository + repos_len + 1, 0); + cvs_output ("/", 1); + } + else if (repository[0] != '\0') + { + cvs_output (repository, 0); + cvs_output ("/", 1); + } + cvs_output (fname, 0); + cvs_output ("\n", 1); + } + + if (repos_len) /* write to logfile */ + (void) fprintf (logfp, "%c %s/%s\n", ch, + repository + repos_len + 1, fname); + else if (repository[0]) + (void) fprintf (logfp, "%c %s/%s\n", ch, repository, fname); + else + (void) fprintf (logfp, "%c %s\n", ch, fname); +} + +/* + * This is the recursive function that walks the argument directory looking + * for sub-directories that have CVS administration files in them and updates + * them recursively. + * + * Note that we do not follow symbolic links here, which is a feature! + */ +static int +import_descend_dir (message, dir, vtag, targc, targv) + char *message; + char *dir; + char *vtag; + int targc; + char *targv[]; +{ + struct saved_cwd cwd; + char *cp; + int ierrno, err; + char *rcs = NULL; + + if (islink (dir)) + return (0); + if (save_cwd (&cwd)) + { + fperrmsg (logfp, 0, 0, "ERROR: cannot get working directory"); + return (1); + } + + /* Concatenate DIR to the end of REPOSITORY. */ + if (repository[0] == '\0') + { + char *new = xstrdup (dir); + free (repository); + repository = new; + } + else + { + char *new = xmalloc (strlen (repository) + strlen (dir) + 10); + strcpy (new, repository); + (void) strcat (new, "/"); + (void) strcat (new, dir); + free (repository); + repository = new; + } + +#ifdef CLIENT_SUPPORT + if (!quiet && !client_active) +#else + if (!quiet) +#endif + error (0, 0, "Importing %s", repository); + + if ( CVS_CHDIR (dir) < 0) + { + ierrno = errno; + fperrmsg (logfp, 0, ierrno, "ERROR: cannot chdir to %s", repository); + error (0, ierrno, "ERROR: cannot chdir to %s", repository); + err = 1; + goto out; + } +#ifdef CLIENT_SUPPORT + if (!client_active && !isdir (repository)) +#else + if (!isdir (repository)) +#endif + { + rcs = xmalloc (strlen (repository) + sizeof (RCSEXT) + 5); + (void) sprintf (rcs, "%s%s", repository, RCSEXT); + if (isfile (repository) || isfile(rcs)) + { + fperrmsg (logfp, 0, 0, + "ERROR: %s is a file, should be a directory!", + repository); + error (0, 0, "ERROR: %s is a file, should be a directory!", + repository); + err = 1; + goto out; + } + if (noexec == 0 && CVS_MKDIR (repository, 0777) < 0) + { + ierrno = errno; + fperrmsg (logfp, 0, ierrno, + "ERROR: cannot mkdir %s -- not added", repository); + error (0, ierrno, + "ERROR: cannot mkdir %s -- not added", repository); + err = 1; + goto out; + } + } + err = import_descend (message, vtag, targc, targv); + out: + if (rcs != NULL) + free (rcs); + if ((cp = strrchr (repository, '/')) != NULL) + *cp = '\0'; + else + repository[0] = '\0'; + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + return (err); +} diff --git a/gnu/dist/cvs/src/lock.c b/gnu/dist/cvs/src/lock.c new file mode 100644 index 00000000000..ef27d114ba1 --- /dev/null +++ b/gnu/dist/cvs/src/lock.c @@ -0,0 +1,941 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Set Lock + * + * Lock file support for CVS. + */ + +/* The node Concurrency in doc/cvs.texinfo has a brief introduction to + how CVS locks function, and some of the user-visible consequences of + their existence. Here is a summary of why they exist (and therefore, + the consequences of hacking CVS to read a repository without creating + locks): + + There are two uses. One is the ability to prevent there from being + two writers at the same time. This is necessary for any number of + reasons (fileattr code, probably others). Commit needs to lock the + whole tree so that nothing happens between the up-to-date check and + the actual checkin. + + The second use is the ability to ensure that there is not a writer + and a reader at the same time (several readers are allowed). Reasons + for this are: + + * Readlocks ensure that once CVS has found a collection of rcs + files using Find_Names, the files will still exist when it reads + them (they may have moved in or out of the attic). + + * Readlocks provide some modicum of consistency, although this is + kind of limited--see the node Concurrency in cvs.texinfo. + + * Readlocks ensure that the RCS file does not change between + RCS_parse and RCS_reparsercsfile time. This one strikes me as + important, although I haven't thought up what bad scenarios might + be. + + * Readlocks ensure that we won't find the file in the state in + which it is in between the calls to add_rcs_file and RCS_checkin in + commit.c (when a file is being added). This state is a state in + which the RCS file parsing routines in rcs.c cannot parse the file. + + * Readlocks ensure that a reader won't try to look at a + half-written fileattr file (fileattr is not updated atomically). + + (see also the description of anonymous read-only access in + "Password authentication security" node in doc/cvs.texinfo). + + While I'm here, I'll try to summarize a few random suggestions + which periodically get made about how locks might be different: + + 1. Check for EROFS. Maybe useful, although in the presence of NFS + EROFS does *not* mean that the file system is unchanging. + + 2. Provide a means to put the cvs locks in some directory apart from + the repository (CVSROOT/locks; a -l option in modules; etc.). + + 3. Provide an option to disable locks for operations which only + read (see above for some of the consequences). + + 4. Have a server internally do the locking. Probably a good + long-term solution, and many people have been working hard on code + changes which would eventually make it possible to have a server + which can handle various connections in one process, but there is + much, much work still to be done before this is feasible. + + 5. Like #4 but use shared memory or something so that the servers + merely need to all be on the same machine. This is a much smaller + change to CVS (it functions much like #2; shared memory might be an + unneeded complication although it presumably would be faster). */ + +#include "cvs.h" +#include <assert.h> + +struct lock { + /* This is the directory in which we may have a lock named by the + readlock variable, a lock named by the writelock variable, and/or + a lock named CVSLCK. The storage is not allocated along with the + struct lock; it is allocated by the Reader_Lock caller or in the + case of writelocks, it is just a pointer to the storage allocated + for the ->key field. */ + char *repository; + /* Do we have a lock named CVSLCK? */ + int have_lckdir; + /* Note there is no way of knowing whether the readlock and writelock + exist. The code which sets the locks doesn't use SIG_beginCrSect + to set a flag like we do for CVSLCK. */ +}; + +static void remove_locks PROTO((void)); +static int readers_exist PROTO((char *repository)); +static int set_lock PROTO ((struct lock *lock, int will_wait)); +static void clear_lock PROTO ((struct lock *lock)); +static void set_lockers_name PROTO((struct stat *statp)); +static int set_writelock_proc PROTO((Node * p, void *closure)); +static int unlock_proc PROTO((Node * p, void *closure)); +static int write_lock PROTO ((struct lock *lock)); +static void lock_simple_remove PROTO ((struct lock *lock)); +static void lock_wait PROTO((char *repository)); +static void lock_obtained PROTO((char *repository)); + +/* Malloc'd array containing the username of the whoever has the lock. + Will always be non-NULL in the cases where it is needed. */ +static char *lockers_name; +/* Malloc'd array specifying name of a readlock within a directory. + Or NULL if none. */ +static char *readlock; +/* Malloc'd array specifying name of a writelock within a directory. + Or NULL if none. */ +static char *writelock; +/* Malloc'd array specifying the name of a CVSLCK file (absolute pathname). + Will always be non-NULL in the cases where it is used. */ +static char *masterlock; +static List *locklist; + +#define L_OK 0 /* success */ +#define L_ERROR 1 /* error condition */ +#define L_LOCKED 2 /* lock owned by someone else */ + +/* This is the (single) readlock which is set by Reader_Lock. The + repository field is NULL if there is no such lock. */ +static struct lock global_readlock; + +/* List of locks set by lock_tree_for_write. This is redundant + with locklist, sort of. */ +static List *lock_tree_list; + +/* If we set locks with lock_dir_for_write, then locked_dir contains + the malloc'd name of the repository directory which we have locked. + locked_list is the same thing packaged into a list and is redundant + with locklist the same way that lock_tree_list is. */ +static char *locked_dir; +static List *locked_list; + +/* LockDir from CVSROOT/config. */ +char *lock_dir; + +static char *lock_name PROTO ((char *repository, char *name)); + +/* Return a newly malloc'd string containing the name of the lock for the + repository REPOSITORY and the lock file name within that directory + NAME. Also create the directories in which to put the lock file + if needed (if we need to, could save system call(s) by doing + that only if the actual operation fails. But for now we'll keep + things simple). */ +static char * +lock_name (repository, name) + char *repository; + char *name; +{ + char *retval; + char *p; + char *q; + char *short_repos; + mode_t save_umask; + int saved_umask = 0; + + if (lock_dir == NULL) + { + /* This is the easy case. Because the lock files go directly + in the repository, no need to create directories or anything. */ + retval = xmalloc (strlen (repository) + strlen (name) + 10); + (void) sprintf (retval, "%s/%s", repository, name); + } + else + { + struct stat sb; + mode_t new_mode = 0; + + /* The interesting part of the repository is the part relative + to CVSROOT. */ + assert (CVSroot_directory != NULL); + assert (strncmp (repository, CVSroot_directory, + strlen (CVSroot_directory)) == 0); + short_repos = repository + strlen (CVSroot_directory) + 1; + + if (strcmp (repository, CVSroot_directory) == 0) + short_repos = "."; + else + assert (short_repos[-1] == '/'); + + retval = xmalloc (strlen (lock_dir) + + strlen (short_repos) + + strlen (name) + + 10); + strcpy (retval, lock_dir); + q = retval + strlen (retval); + *q++ = '/'; + + strcpy (q, short_repos); + + /* In the common case, where the directory already exists, let's + keep it to one system call. */ + if (CVS_STAT (retval, &sb) < 0) + { + /* If we need to be creating more than one directory, we'll + get the existence_error here. */ + if (!existence_error (errno)) + error (1, errno, "cannot stat directory %s", retval); + } + else + { + if (S_ISDIR (sb.st_mode)) + goto created; + else + error (1, 0, "%s is not a directory", retval); + } + + /* Now add the directories one at a time, so we can create + them if needed. + + The idea behind the new_mode stuff is that the directory we + end up creating will inherit permissions from its parent + directory (we re-set new_mode with each EEXIST). CVSUMASK + isn't right, because typically the reason for LockDir is to + use a different set of permissions. We probably want to + inherit group ownership also (but we don't try to deal with + that, some systems do it for us either always or when g+s is on). + + We don't try to do anything about the permissions on the lock + files themselves. The permissions don't really matter so much + because the locks will generally be removed by the process + which created them. */ + + if (CVS_STAT (lock_dir, &sb) < 0) + error (1, errno, "cannot stat %s", lock_dir); + new_mode = sb.st_mode; + save_umask = umask (0000); + saved_umask = 1; + + p = short_repos; + while (1) + { + while (!ISDIRSEP (*p) && *p != '\0') + ++p; + if (ISDIRSEP (*p)) + { + strncpy (q, short_repos, p - short_repos); + q[p - short_repos] = '\0'; + if (!ISDIRSEP (q[p - short_repos - 1]) + && CVS_MKDIR (retval, new_mode) < 0) + { + int saved_errno = errno; + if (saved_errno != EEXIST) + error (1, errno, "cannot make directory %s", retval); + else + { + if (CVS_STAT (retval, &sb) < 0) + error (1, errno, "cannot stat %s", retval); + new_mode = sb.st_mode; + } + } + ++p; + } + else + { + strcpy (q, short_repos); + if (CVS_MKDIR (retval, new_mode) < 0 + && errno != EEXIST) + error (1, errno, "cannot make directory %s", retval); + goto created; + } + } + created:; + + strcat (retval, "/"); + strcat (retval, name); + + if (saved_umask) + { + assert (umask (save_umask) == 0000); + saved_umask = 0; + } + } + return retval; +} + +/* + * Clean up all outstanding locks + */ +void +Lock_Cleanup () +{ + /* FIXME: error handling here is kind of bogus; we sometimes will call + error, which in turn can call us again. For the moment work around + this by refusing to reenter this function (this is a kludge). */ + /* FIXME-reentrancy: the workaround isn't reentrant. */ + static int in_lock_cleanup = 0; + + if (in_lock_cleanup) + return; + in_lock_cleanup = 1; + + remove_locks (); + + dellist (&lock_tree_list); + + if (locked_dir != NULL) + { + dellist (&locked_list); + free (locked_dir); + locked_dir = NULL; + locked_list = NULL; + } + in_lock_cleanup = 0; +} + +/* + * Remove locks without discarding the lock information + */ +static void +remove_locks () +{ + /* clean up simple locks (if any) */ + if (global_readlock.repository != NULL) + { + lock_simple_remove (&global_readlock); + global_readlock.repository = NULL; + } + + /* clean up multiple locks (if any) */ + if (locklist != (List *) NULL) + { + (void) walklist (locklist, unlock_proc, NULL); + locklist = (List *) NULL; + } +} + +/* + * walklist proc for removing a list of locks + */ +static int +unlock_proc (p, closure) + Node *p; + void *closure; +{ + lock_simple_remove ((struct lock *)p->data); + return (0); +} + +/* Remove the lock files. */ +static void +lock_simple_remove (lock) + struct lock *lock; +{ + char *tmp; + + /* If readlock is set, the lock directory *might* have been created, but + since Reader_Lock doesn't use SIG_beginCrSect the way that set_lock + does, we don't know that. That is why we need to check for + existence_error here. */ + if (readlock != NULL) + { + tmp = lock_name (lock->repository, readlock); + if ( CVS_UNLINK (tmp) < 0 && ! existence_error (errno)) + error (0, errno, "failed to remove lock %s", tmp); + free (tmp); + } + + /* If writelock is set, the lock directory *might* have been created, but + since write_lock doesn't use SIG_beginCrSect the way that set_lock + does, we don't know that. That is why we need to check for + existence_error here. */ + if (writelock != NULL) + { + tmp = lock_name (lock->repository, writelock); + if ( CVS_UNLINK (tmp) < 0 && ! existence_error (errno)) + error (0, errno, "failed to remove lock %s", tmp); + free (tmp); + } + + if (lock->have_lckdir) + { + tmp = lock_name (lock->repository, CVSLCK); + SIG_beginCrSect (); + if (CVS_RMDIR (tmp) < 0) + error (0, errno, "failed to remove lock dir %s", tmp); + lock->have_lckdir = 0; + SIG_endCrSect (); + free (tmp); + } +} + +/* + * Create a lock file for readers + */ +int +Reader_Lock (xrepository) + char *xrepository; +{ + int err = 0; + FILE *fp; + char *tmp; + + if (noexec) + return (0); + + /* we only do one directory at a time for read locks! */ + if (global_readlock.repository != NULL) + { + error (0, 0, "Reader_Lock called while read locks set - Help!"); + return (1); + } + + if (readlock == NULL) + { + readlock = xmalloc (strlen (hostname) + sizeof (CVSRFL) + 40); + (void) sprintf (readlock, +#ifdef HAVE_LONG_FILE_NAMES + "%s.%s.%ld", CVSRFL, hostname, +#else + "%s.%ld", CVSRFL, +#endif + (long) getpid ()); + } + + /* remember what we're locking (for Lock_Cleanup) */ + global_readlock.repository = xrepository; + + /* get the lock dir for our own */ + if (set_lock (&global_readlock, 1) != L_OK) + { + error (0, 0, "failed to obtain dir lock in repository `%s'", + xrepository); + if (readlock != NULL) + free (readlock); + readlock = NULL; + /* We don't set global_readlock.repository to NULL. I think this + only works because recurse.c will give a fatal error if we return + a nonzero value. */ + return (1); + } + + /* write a read-lock */ + tmp = lock_name (xrepository, readlock); + if ((fp = CVS_FOPEN (tmp, "w+")) == NULL || fclose (fp) == EOF) + { + error (0, errno, "cannot create read lock in repository `%s'", + xrepository); + if (readlock != NULL) + free (readlock); + readlock = NULL; + err = 1; + } + free (tmp); + + /* free the lock dir */ + clear_lock (&global_readlock); + + return (err); +} + +/* + * Lock a list of directories for writing + */ +static char *lock_error_repos; +static int lock_error; + +static int Writer_Lock PROTO ((List * list)); + +static int +Writer_Lock (list) + List *list; +{ + char *wait_repos; + + if (noexec) + return (0); + + /* We only know how to do one list at a time */ + if (locklist != (List *) NULL) + { + error (0, 0, "Writer_Lock called while write locks set - Help!"); + return (1); + } + + wait_repos = NULL; + for (;;) + { + /* try to lock everything on the list */ + lock_error = L_OK; /* init for set_writelock_proc */ + lock_error_repos = (char *) NULL; /* init for set_writelock_proc */ + locklist = list; /* init for Lock_Cleanup */ + if (lockers_name != NULL) + free (lockers_name); + lockers_name = xstrdup ("unknown"); + + (void) walklist (list, set_writelock_proc, NULL); + + switch (lock_error) + { + case L_ERROR: /* Real Error */ + if (wait_repos != NULL) + free (wait_repos); + Lock_Cleanup (); /* clean up any locks we set */ + error (0, 0, "lock failed - giving up"); + return (1); + + case L_LOCKED: /* Someone already had a lock */ + remove_locks (); /* clean up any locks we set */ + lock_wait (lock_error_repos); /* sleep a while and try again */ + wait_repos = xstrdup (lock_error_repos); + continue; + + case L_OK: /* we got the locks set */ + if (wait_repos != NULL) + { + lock_obtained (wait_repos); + free (wait_repos); + } + return (0); + + default: + if (wait_repos != NULL) + free (wait_repos); + error (0, 0, "unknown lock status %d in Writer_Lock", + lock_error); + return (1); + } + } +} + +/* + * walklist proc for setting write locks + */ +static int +set_writelock_proc (p, closure) + Node *p; + void *closure; +{ + /* if some lock was not OK, just skip this one */ + if (lock_error != L_OK) + return (0); + + /* apply the write lock */ + lock_error_repos = p->key; + lock_error = write_lock ((struct lock *)p->data); + return (0); +} + +/* + * Create a lock file for writers returns L_OK if lock set ok, L_LOCKED if + * lock held by someone else or L_ERROR if an error occurred + */ +static int +write_lock (lock) + struct lock *lock; +{ + int status; + FILE *fp; + char *tmp; + + if (writelock == NULL) + { + writelock = xmalloc (strlen (hostname) + sizeof (CVSWFL) + 40); + (void) sprintf (writelock, +#ifdef HAVE_LONG_FILE_NAMES + "%s.%s.%ld", CVSWFL, hostname, +#else + "%s.%ld", CVSWFL, +#endif + (long) getpid()); + } + + /* make sure the lock dir is ours (not necessarily unique to us!) */ + status = set_lock (lock, 0); + if (status == L_OK) + { + /* we now own a writer - make sure there are no readers */ + if (readers_exist (lock->repository)) + { + /* clean up the lock dir if we created it */ + if (status == L_OK) + { + clear_lock (lock); + } + + /* indicate we failed due to read locks instead of error */ + return (L_LOCKED); + } + + /* write the write-lock file */ + tmp = lock_name (lock->repository, writelock); + if ((fp = CVS_FOPEN (tmp, "w+")) == NULL || fclose (fp) == EOF) + { + int xerrno = errno; + + if ( CVS_UNLINK (tmp) < 0 && ! existence_error (errno)) + error (0, errno, "failed to remove lock %s", tmp); + + /* free the lock dir if we created it */ + if (status == L_OK) + { + clear_lock (lock); + } + + /* return the error */ + error (0, xerrno, "cannot create write lock in repository `%s'", + lock->repository); + free (tmp); + return (L_ERROR); + } + free (tmp); + return (L_OK); + } + else + return (status); +} + +/* + * readers_exist() returns 0 if there are no reader lock files remaining in + * the repository; else 1 is returned, to indicate that the caller should + * sleep a while and try again. + */ +static int +readers_exist (repository) + char *repository; +{ + char *line; + DIR *dirp; + struct dirent *dp; + struct stat sb; + int ret = 0; + +#ifdef CVS_FUDGELOCKS +again: +#endif + + if ((dirp = CVS_OPENDIR (repository)) == NULL) + error (1, 0, "cannot open directory %s", repository); + + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (CVS_FNMATCH (CVSRFLPAT, dp->d_name, 0) == 0) + { +#ifdef CVS_FUDGELOCKS + time_t now; + (void) time (&now); +#endif + + line = xmalloc (strlen (repository) + strlen (dp->d_name) + 5); + (void) sprintf (line, "%s/%s", repository, dp->d_name); + if ( CVS_STAT (line, &sb) != -1) + { +#ifdef CVS_FUDGELOCKS + /* + * If the create time of the file is more than CVSLCKAGE + * seconds ago, try to clean-up the lock file, and if + * successful, re-open the directory and try again. + */ + if (now >= (sb.st_ctime + CVSLCKAGE) && CVS_UNLINK (line) != -1) + { + (void) closedir (dirp); + free (line); + goto again; + } +#endif + set_lockers_name (&sb); + } + else + { + /* If the file doesn't exist, it just means that it disappeared + between the time we did the readdir and the time we did + the stat. */ + if (!existence_error (errno)) + error (0, errno, "cannot stat %s", line); + } + errno = 0; + free (line); + + ret = 1; + break; + } + errno = 0; + } + if (errno != 0) + error (0, errno, "error reading directory %s", repository); + + closedir (dirp); + return (ret); +} + +/* + * Set the static variable lockers_name appropriately, based on the stat + * structure passed in. + */ +static void +set_lockers_name (statp) + struct stat *statp; +{ + struct passwd *pw; + + if (lockers_name != NULL) + free (lockers_name); + if ((pw = (struct passwd *) getpwuid (statp->st_uid)) != + (struct passwd *) NULL) + { + lockers_name = xstrdup (pw->pw_name); + } + else + { + lockers_name = xmalloc (20); + (void) sprintf (lockers_name, "uid%lu", (unsigned long) statp->st_uid); + } +} + +/* + * Persistently tries to make the directory "lckdir",, which serves as a + * lock. If the create time on the directory is greater than CVSLCKAGE + * seconds old, just try to remove the directory. + */ +static int +set_lock (lock, will_wait) + struct lock *lock; + int will_wait; +{ + int waited; + struct stat sb; + mode_t omask; +#ifdef CVS_FUDGELOCKS + time_t now; +#endif + + if (masterlock != NULL) + free (masterlock); + masterlock = lock_name (lock->repository, CVSLCK); + + /* + * Note that it is up to the callers of set_lock() to arrange for signal + * handlers that do the appropriate things, like remove the lock + * directory before they exit. + */ + waited = 0; + lock->have_lckdir = 0; + for (;;) + { + int status = -1; + omask = umask (cvsumask); + SIG_beginCrSect (); + if (CVS_MKDIR (masterlock, 0777) == 0) + { + lock->have_lckdir = 1; + SIG_endCrSect (); + status = L_OK; + if (waited) + lock_obtained (lock->repository); + goto out; + } + SIG_endCrSect (); + out: + (void) umask (omask); + if (status != -1) + return status; + + if (errno != EEXIST) + { + error (0, errno, + "failed to create lock directory in repository `%s'", + lock->repository); + return (L_ERROR); + } + + /* Find out who owns the lock. If the lock directory is + non-existent, re-try the loop since someone probably just + removed it (thus releasing the lock). */ + if (CVS_STAT (masterlock, &sb) < 0) + { + if (existence_error (errno)) + continue; + + error (0, errno, "couldn't stat lock directory `%s'", masterlock); + return (L_ERROR); + } + +#ifdef CVS_FUDGELOCKS + /* + * If the create time of the directory is more than CVSLCKAGE seconds + * ago, try to clean-up the lock directory, and if successful, just + * quietly retry to make it. + */ + (void) time (&now); + if (now >= (sb.st_ctime + CVSLCKAGE)) + { + if (CVS_RMDIR (masterlock) >= 0) + continue; + } +#endif + + /* set the lockers name */ + set_lockers_name (&sb); + + /* if he wasn't willing to wait, return an error */ + if (!will_wait) + return (L_LOCKED); + lock_wait (lock->repository); + waited = 1; + } +} + +/* + * Clear master lock. We don't have to recompute the lock name since + * clear_lock is never called except after a successful set_lock(). + */ +static void +clear_lock (lock) + struct lock *lock; +{ + SIG_beginCrSect (); + if (CVS_RMDIR (masterlock) < 0) + error (0, errno, "failed to remove lock dir `%s'", masterlock); + lock->have_lckdir = 0; + SIG_endCrSect (); +} + +/* + * Print out a message that the lock is still held, then sleep a while. + */ +static void +lock_wait (repos) + char *repos; +{ + time_t now; + char *msg; + + (void) time (&now); + msg = xmalloc (100 + strlen (lockers_name) + strlen (repos)); + sprintf (msg, "[%8.8s] waiting for %s's lock in %s", ctime (&now) + 11, + lockers_name, repos); + error (0, 0, "%s", msg); + /* Call cvs_flusherr to ensure that the user sees this message as + soon as possible. */ + cvs_flusherr (); + free (msg); + (void) sleep (CVSLCKSLEEP); +} + +/* + * Print out a message when we obtain a lock. + */ +static void +lock_obtained (repos) + char *repos; +{ + time_t now; + char *msg; + + (void) time (&now); + msg = xmalloc (100 + strlen (repos)); + sprintf (msg, "[%8.8s] obtained lock in %s", ctime (&now) + 11, repos); + error (0, 0, "%s", msg); + /* Call cvs_flusherr to ensure that the user sees this message as + soon as possible. */ + cvs_flusherr (); + free (msg); +} + +static int lock_filesdoneproc PROTO ((void *callerdat, int err, + char *repository, char *update_dir, + List *entries)); + +/* + * Create a list of repositories to lock + */ +/* ARGSUSED */ +static int +lock_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + char *repository; + char *update_dir; + List *entries; +{ + Node *p; + + p = getnode (); + p->type = LOCK; + p->key = xstrdup (repository); + p->data = xmalloc (sizeof (struct lock)); + ((struct lock *)p->data)->repository = p->key; + ((struct lock *)p->data)->have_lckdir = 0; + + /* FIXME-KRP: this error condition should not simply be passed by. */ + if (p->key == NULL || addnode (lock_tree_list, p) != 0) + freenode (p); + return (err); +} + +void +lock_tree_for_write (argc, argv, local, aflag) + int argc; + char **argv; + int local; + int aflag; +{ + int err; + /* + * Run the recursion processor to find all the dirs to lock and lock all + * the dirs + */ + lock_tree_list = getlist (); + err = start_recursion ((FILEPROC) NULL, lock_filesdoneproc, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, + argv, local, W_LOCAL, aflag, 0, (char *) NULL, 0); + sortlist (lock_tree_list, fsortcmp); + if (Writer_Lock (lock_tree_list) != 0) + error (1, 0, "lock failed - giving up"); +} + +/* Lock a single directory in REPOSITORY. It is OK to call this if + a lock has been set with lock_dir_for_write; the new lock will replace + the old one. If REPOSITORY is NULL, don't do anything. */ +void +lock_dir_for_write (repository) + char *repository; +{ + if (repository != NULL + && (locked_dir == NULL + || strcmp (locked_dir, repository) != 0)) + { + Node *node; + + if (locked_dir != NULL) + Lock_Cleanup (); + + locked_dir = xstrdup (repository); + locked_list = getlist (); + node = getnode (); + node->type = LOCK; + node->key = xstrdup (repository); + node->data = xmalloc (sizeof (struct lock)); + ((struct lock *)node->data)->repository = node->key; + ((struct lock *)node->data)->have_lckdir = 0; + + (void) addnode (locked_list, node); + Writer_Lock (locked_list); + } +} diff --git a/gnu/dist/cvs/src/log.c b/gnu/dist/cvs/src/log.c new file mode 100644 index 00000000000..81eba23aec4 --- /dev/null +++ b/gnu/dist/cvs/src/log.c @@ -0,0 +1,1466 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Print Log Information + * + * Prints the RCS "log" (rlog) information for the specified files. With no + * argument, prints the log information for all the files in the directory + * (recursive by default). + */ + +#include "cvs.h" + +/* This structure holds information parsed from the -r option. */ + +struct option_revlist +{ + /* The next -r option. */ + struct option_revlist *next; + /* The first revision to print. This is NULL if the range is + :rev, or if no revision is given. */ + char *first; + /* The last revision to print. This is NULL if the range is rev:, + or if no revision is given. If there is no colon, first and + last are the same. */ + char *last; + /* Nonzero if there was a trailing `.', which means to print only + the head revision of a branch. */ + int branchhead; +}; + +/* This structure holds information derived from option_revlist given + a particular RCS file. */ + +struct revlist +{ + /* The next pair. */ + struct revlist *next; + /* The first numeric revision to print. */ + char *first; + /* The last numeric revision to print. */ + char *last; + /* The number of fields in these revisions (one more than + numdots). */ + int fields; +}; + +/* This structure holds information parsed from the -d option. */ + +struct datelist +{ + /* The next date. */ + struct datelist *next; + /* The starting date. */ + char *start; + /* The ending date. */ + char *end; + /* Nonzero if the range is inclusive rather than exclusive. */ + int inclusive; +}; + +/* This structure is used to pass information through start_recursion. */ +struct log_data +{ + /* Nonzero if the -R option was given, meaning that only the name + of the RCS file should be printed. */ + int nameonly; + /* Nonzero if the -h option was given, meaning that only header + information should be printed. */ + int header; + /* Nonzero if the -t option was given, meaning that only the + header and the descriptive text should be printed. */ + int long_header; + /* Nonzero if the -N option was seen, meaning that tag information + should not be printed. */ + int notags; + /* Nonzero if the -b option was seen, meaning that only revisions + on the default branch should be printed. */ + int default_branch; + /* If not NULL, the value given for the -r option, which lists + sets of revisions to be printed. */ + struct option_revlist *revlist; + /* If not NULL, the date pairs given for the -d option, which + select date ranges to print. */ + struct datelist *datelist; + /* If not NULL, the single dates given for the -d option, which + select specific revisions to print based on a date. */ + struct datelist *singledatelist; + /* If not NULL, the list of states given for the -s option, which + only prints revisions of given states. */ + List *statelist; + /* If not NULL, the list of login names given for the -w option, + which only prints revisions checked in by given users. */ + List *authorlist; +}; + +/* This structure is used to pass information through walklist. */ +struct log_data_and_rcs +{ + struct log_data *log_data; + struct revlist *revlist; + RCSNode *rcs; +}; + +static Dtype log_dirproc PROTO ((void *callerdat, char *dir, + char *repository, char *update_dir, + List *entries)); +static int log_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static struct option_revlist *log_parse_revlist PROTO ((const char *)); +static void log_parse_date PROTO ((struct log_data *, const char *)); +static void log_parse_list PROTO ((List **, const char *)); +static struct revlist *log_expand_revlist PROTO ((RCSNode *, + struct option_revlist *, + int)); +static void log_free_revlist PROTO ((struct revlist *)); +static int log_version_requested PROTO ((struct log_data *, struct revlist *, + RCSNode *, RCSVers *)); +static int log_symbol PROTO ((Node *, void *)); +static int log_count PROTO ((Node *, void *)); +static int log_fix_singledate PROTO ((Node *, void *)); +static int log_count_print PROTO ((Node *, void *)); +static void log_tree PROTO ((struct log_data *, struct revlist *, + RCSNode *, const char *)); +static void log_abranch PROTO ((struct log_data *, struct revlist *, + RCSNode *, const char *)); +static void log_version PROTO ((struct log_data *, struct revlist *, + RCSNode *, RCSVers *, int)); +static int log_branch PROTO ((Node *, void *)); +static int version_compare PROTO ((const char *, const char *, int)); + +static const char *const log_usage[] = +{ + "Usage: %s %s [-lRhtNb] [-r[revisions]] [-d dates] [-s states]\n", + " [-w[logins]] [files...]\n", + "\t-l\tLocal directory only, no recursion.\n", + "\t-R\tOnly print name of RCS file.\n", + "\t-h\tOnly print header.\n", + "\t-t\tOnly print header and descriptive text.\n", + "\t-N\tDo not list tags.\n", + "\t-b\tOnly list revisions on the default branch.\n", + "\t-r[revisions]\tSpecify revision(s)s to list.\n", + "\t-d dates\tSpecify dates (D1<D2 for range, D for latest before).\n", + "\t-s states\tOnly list revisions with specified states.\n", + "\t-w[logins]\tOnly list revisions checked in by specified logins.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* Helper function for send_arg_list. */ +static int send_one PROTO ((Node *, void *)); + +static int +send_one (node, closure) + Node *node; + void *closure; +{ + char *option = (char *) closure; + + send_to_server ("Argument ", 0); + send_to_server (option, 0); + if (strcmp (node->key, "@@MYSELF") == 0) + /* It is a bare -w option. Note that we must send it as + -w rather than messing with getcaller() or something (which on + the client will return garbage). */ + ; + else + send_to_server (node->key, 0); + send_to_server ("\012", 0); + return 0; +} + +/* For each element in ARG, send an argument consisting of OPTION + concatenated with that element. */ +static void send_arg_list PROTO ((char *, List *)); + +static void +send_arg_list (option, arg) + char *option; + List *arg; +{ + if (arg == NULL) + return; + walklist (arg, send_one, (void *)option); +} + +int +cvslog (argc, argv) + int argc; + char **argv; +{ + int c; + int err = 0; + int local = 0; + struct log_data log_data; + struct option_revlist *rl, **prl; + + if (argc == -1) + usage (log_usage); + + memset (&log_data, 0, sizeof log_data); + + optind = 0; + while ((c = getopt (argc, argv, "+bd:hlNRr::s:tw::")) != -1) + { + switch (c) + { + case 'b': + log_data.default_branch = 1; + break; + case 'd': + log_parse_date (&log_data, optarg); + break; + case 'h': + log_data.header = 1; + break; + case 'l': + local = 1; + break; + case 'N': + log_data.notags = 1; + break; + case 'R': + log_data.nameonly = 1; + break; + case 'r': + rl = log_parse_revlist (optarg); + for (prl = &log_data.revlist; + *prl != NULL; + prl = &(*prl)->next) + ; + *prl = rl; + break; + case 's': + log_parse_list (&log_data.statelist, optarg); + break; + case 't': + log_data.long_header = 1; + break; + case 'w': + if (optarg != NULL) + log_parse_list (&log_data.authorlist, optarg); + else + log_parse_list (&log_data.authorlist, "@@MYSELF"); + break; + case '?': + default: + usage (log_usage); + break; + } + } + + wrap_setup (); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + struct datelist *p; + struct option_revlist *rp; + char datetmp[MAXDATELEN]; + + /* We're the local client. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (log_data.default_branch) + send_arg ("-b"); + + for (p = log_data.datelist; p != NULL; p = p->next) + { + send_to_server ("Argument -d\012", 0); + send_to_server ("Argument ", 0); + date_to_internet (datetmp, p->start); + send_to_server (datetmp, 0); + if (p->inclusive) + send_to_server ("<=", 0); + else + send_to_server ("<", 0); + date_to_internet (datetmp, p->end); + send_to_server (datetmp, 0); + send_to_server ("\012", 0); + } + for (p = log_data.singledatelist; p != NULL; p = p->next) + { + send_to_server ("Argument -d\012", 0); + send_to_server ("Argument ", 0); + date_to_internet (datetmp, p->end); + send_to_server (datetmp, 0); + send_to_server ("\012", 0); + } + + if (log_data.header) + send_arg ("-h"); + if (local) + send_arg("-l"); + if (log_data.notags) + send_arg("-N"); + if (log_data.nameonly) + send_arg("-R"); + if (log_data.long_header) + send_arg("-t"); + + for (rp = log_data.revlist; rp != NULL; rp = rp->next) + { + send_to_server ("Argument -r", 0); + if (rp->branchhead) + { + if (rp->first != NULL) + send_to_server (rp->first, 0); + send_to_server (".", 1); + } + else + { + if (rp->first != NULL) + send_to_server (rp->first, 0); + send_to_server (":", 1); + if (rp->last != NULL) + send_to_server (rp->last, 0); + } + send_to_server ("\012", 0); + } + send_arg_list ("-s", log_data.statelist); + send_arg_list ("-w", log_data.authorlist); + + send_files (argc - optind, argv + optind, local, 0, SEND_NO_CONTENTS); + send_file_names (argc - optind, argv + optind, SEND_EXPAND_WILD); + + send_to_server ("log\012", 0); + err = get_responses_and_close (); + return err; + } +#endif + + /* OK, now that we know we are local/server, we can resolve @@MYSELF + into our user name. */ + if (findnode (log_data.authorlist, "@@MYSELF") != NULL) + log_parse_list (&log_data.authorlist, getcaller ()); + + err = start_recursion (log_fileproc, (FILESDONEPROC) NULL, log_dirproc, + (DIRLEAVEPROC) NULL, (void *) &log_data, + argc - optind, argv + optind, local, + W_LOCAL | W_REPOS | W_ATTIC, 0, 1, + (char *) NULL, 1); + return (err); +} + +/* + * Parse a revision list specification. + */ + +static struct option_revlist * +log_parse_revlist (argstring) + const char *argstring; +{ + char *copy; + struct option_revlist *ret, **pr; + + /* Unfortunately, rlog accepts -r without an argument to mean that + latest revision on the default branch, so we must support that + for compatibility. */ + if (argstring == NULL) + argstring = ""; + + ret = NULL; + pr = &ret; + + /* Copy the argument into memory so that we can change it. We + don't want to change the argument because, at least as of this + writing, we will use it if we send the arguments to the server. + We never bother to free up our copy. */ + copy = xstrdup (argstring); + while (copy != NULL) + { + char *comma; + struct option_revlist *r; + + comma = strchr (copy, ','); + if (comma != NULL) + *comma++ = '\0'; + + r = (struct option_revlist *) xmalloc (sizeof *r); + r->next = NULL; + r->first = copy; + r->branchhead = 0; + r->last = strchr (copy, ':'); + if (r->last != NULL) + *r->last++ = '\0'; + else + { + r->last = r->first; + if (r->first[0] != '\0' && r->first[strlen (r->first) - 1] == '.') + { + r->branchhead = 1; + r->first[strlen (r->first) - 1] = '\0'; + } + } + + if (*r->first == '\0') + r->first = NULL; + if (*r->last == '\0') + r->last = NULL; + + *pr = r; + pr = &r->next; + + copy = comma; + } + + return ret; +} + +/* + * Parse a date specification. + */ +static void +log_parse_date (log_data, argstring) + struct log_data *log_data; + const char *argstring; +{ + char *orig_copy, *copy; + + /* Copy the argument into memory so that we can change it. We + don't want to change the argument because, at least as of this + writing, we will use it if we send the arguments to the server. */ + copy = xstrdup (argstring); + orig_copy = copy; + while (copy != NULL) + { + struct datelist *nd, **pd; + char *cpend, *cp, *ds, *de; + + nd = (struct datelist *) xmalloc (sizeof *nd); + + cpend = strchr (copy, ';'); + if (cpend != NULL) + *cpend++ = '\0'; + + pd = &log_data->datelist; + nd->inclusive = 0; + + if ((cp = strchr (copy, '>')) != NULL) + { + *cp++ = '\0'; + if (*cp == '=') + { + ++cp; + nd->inclusive = 1; + } + ds = cp; + de = copy; + } + else if ((cp = strchr (copy, '<')) != NULL) + { + *cp++ = '\0'; + if (*cp == '=') + { + ++cp; + nd->inclusive = 1; + } + ds = copy; + de = cp; + } + else + { + ds = NULL; + de = copy; + pd = &log_data->singledatelist; + } + + if (ds == NULL) + nd->start = NULL; + else if (*ds != '\0') + nd->start = Make_Date (ds); + else + { + /* 1970 was the beginning of time, as far as get_date and + Make_Date are concerned. FIXME: That is true only if time_t + is a POSIX-style time and there is nothing in ANSI that + mandates that. It would be cleaner to set a flag saying + whether or not there is a start date. */ + nd->start = Make_Date ("1/1/1970 UTC"); + } + + if (*de != '\0') + nd->end = Make_Date (de); + else + { + /* We want to set the end date to some time sufficiently far + in the future to pick up all revisions that have been + created since the specified date and the time `cvs log' + completes. FIXME: The date in question only makes sense + if time_t is a POSIX-style time and it is 32 bits + and signed. We should instead be setting a flag saying + whether or not there is an end date. Note that using + something like "next week" would break the testsuite (and, + perhaps less importantly, loses if the clock is set grossly + wrong). */ + nd->end = Make_Date ("2038-01-01"); + } + + nd->next = *pd; + *pd = nd; + + copy = cpend; + } + + free (orig_copy); +} + +/* + * Parse a comma separated list of items, and add each one to *PLIST. + */ +static void +log_parse_list (plist, argstring) + List **plist; + const char *argstring; +{ + while (1) + { + Node *p; + char *cp; + + p = getnode (); + + cp = strchr (argstring, ','); + if (cp == NULL) + p->key = xstrdup (argstring); + else + { + size_t len; + + len = cp - argstring; + p->key = xmalloc (len + 1); + strncpy (p->key, argstring, len); + p->key[len + 1] = '\0'; + } + + if (*plist == NULL) + *plist = getlist (); + if (addnode (*plist, p) != 0) + freenode (p); + + if (cp == NULL) + break; + + argstring = cp + 1; + } +} + +static int printlock_proc PROTO ((Node *, void *)); + +static int +printlock_proc (lock, foo) + Node *lock; + void *foo; +{ + cvs_output ("\n\t", 2); + cvs_output (lock->data, 0); + cvs_output (": ", 2); + cvs_output (lock->key, 0); + return 0; +} + +/* + * Do an rlog on a file + */ +static int +log_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + struct log_data *log_data = (struct log_data *) callerdat; + Node *p; + RCSNode *rcsfile; + char buf[50]; + struct revlist *revlist; + struct log_data_and_rcs log_data_and_rcs; + + if ((rcsfile = finfo->rcs) == NULL) + { + /* no rcs file. What *do* we know about this file? */ + p = findnode (finfo->entries, finfo->file); + if (p != NULL) + { + Entnode *e; + + e = (Entnode *) p->data; + if (e->version[0] == '0' && e->version[1] == '\0') + { + if (!really_quiet) + error (0, 0, "%s has been added, but not committed", + finfo->file); + return(0); + } + } + + if (!really_quiet) + error (0, 0, "nothing known about %s", finfo->file); + + return (1); + } + + if (log_data->nameonly) + { + cvs_output (rcsfile->path, 0); + cvs_output ("\n", 1); + return 0; + } + + /* We will need all the information in the RCS file. */ + RCS_fully_parse (rcsfile); + + /* Turn any symbolic revisions in the revision list into numeric + revisions. */ + revlist = log_expand_revlist (rcsfile, log_data->revlist, + log_data->default_branch); + + /* The output here is intended to be exactly compatible with the + output of rlog. I'm not sure whether this code should be here + or in rcs.c; I put it here because it is specific to the log + function, even though it uses information gathered by the + functions in rcs.c. */ + + cvs_output ("\n", 1); + + cvs_output ("RCS file: ", 0); + cvs_output (rcsfile->path, 0); + + cvs_output ("\nWorking file: ", 0); + if (finfo->update_dir[0] == '\0') + cvs_output (finfo->file, 0); + else + { + cvs_output (finfo->update_dir, 0); + cvs_output ("/", 0); + cvs_output (finfo->file, 0); + + } + + cvs_output ("\nhead:", 0); + if (rcsfile->head != NULL) + { + cvs_output (" ", 1); + cvs_output (rcsfile->head, 0); + } + + cvs_output ("\nbranch:", 0); + if (rcsfile->branch != NULL) + { + cvs_output (" ", 1); + cvs_output (rcsfile->branch, 0); + } + + cvs_output ("\nlocks:", 0); + if (rcsfile->strict_locks) + cvs_output (" strict", 0); + walklist (RCS_getlocks (rcsfile), printlock_proc, NULL); + + cvs_output ("\naccess list:", 0); + if (rcsfile->access != NULL) + { + const char *cp; + + cp = rcsfile->access; + while (*cp != '\0') + { + const char *cp2; + + cvs_output ("\n\t", 2); + cp2 = cp; + while (! isspace ((unsigned char) *cp2) && *cp2 != '\0') + ++cp2; + cvs_output (cp, cp2 - cp); + cp = cp2; + while (isspace ((unsigned char) *cp) && *cp != '\0') + ++cp; + } + } + + if (! log_data->notags) + { + List *syms; + + cvs_output ("\nsymbolic names:", 0); + syms = RCS_symbols (rcsfile); + walklist (syms, log_symbol, NULL); + } + + cvs_output ("\nkeyword substitution: ", 0); + if (rcsfile->expand == NULL) + cvs_output ("kv", 2); + else + cvs_output (rcsfile->expand, 0); + + cvs_output ("\ntotal revisions: ", 0); + sprintf (buf, "%d", walklist (rcsfile->versions, log_count, NULL)); + cvs_output (buf, 0); + + if (! log_data->header && ! log_data->long_header) + { + cvs_output (";\tselected revisions: ", 0); + + log_data_and_rcs.log_data = log_data; + log_data_and_rcs.revlist = revlist; + log_data_and_rcs.rcs = rcsfile; + + /* If any single dates were specified, we need to identify the + revisions they select. Each one selects the single + revision, which is otherwise selected, of that date or + earlier. The log_fix_singledate routine will fill in the + start date for each specific revision. */ + if (log_data->singledatelist != NULL) + walklist (rcsfile->versions, log_fix_singledate, + (void *) &log_data_and_rcs); + + sprintf (buf, "%d", walklist (rcsfile->versions, log_count_print, + (void *) &log_data_and_rcs)); + cvs_output (buf, 0); + } + + cvs_output ("\n", 1); + + if (! log_data->header || log_data->long_header) + { + cvs_output ("description:\n", 0); + if (rcsfile->desc != NULL) + cvs_output (rcsfile->desc, 0); + } + + if (! log_data->header && ! log_data->long_header && rcsfile->head != NULL) + { + p = findnode (rcsfile->versions, rcsfile->head); + if (p == NULL) + error (1, 0, "can not find head revision in `%s'", + finfo->fullname); + while (p != NULL) + { + RCSVers *vers; + + vers = (RCSVers *) p->data; + log_version (log_data, revlist, rcsfile, vers, 1); + if (vers->next == NULL) + p = NULL; + else + { + p = findnode (rcsfile->versions, vers->next); + if (p == NULL) + error (1, 0, "can not find next revision `%s' in `%s'", + vers->next, finfo->fullname); + } + } + + log_tree (log_data, revlist, rcsfile, rcsfile->head); + } + + cvs_output("\ +=============================================================================\n", + 0); + + /* Free up the new revlist and restore the old one. */ + log_free_revlist (revlist); + + /* If singledatelist is not NULL, free up the start dates we added + to it. */ + if (log_data->singledatelist != NULL) + { + struct datelist *d; + + for (d = log_data->singledatelist; d != NULL; d = d->next) + { + if (d->start != NULL) + free (d->start); + d->start = NULL; + } + } + + return 0; +} + +/* + * Fix up a revision list in order to compare it against versions. + * Expand any symbolic revisions. + */ +static struct revlist * +log_expand_revlist (rcs, revlist, default_branch) + RCSNode *rcs; + struct option_revlist *revlist; + int default_branch; +{ + struct option_revlist *r; + struct revlist *ret, **pr; + + ret = NULL; + pr = &ret; + for (r = revlist; r != NULL; r = r->next) + { + struct revlist *nr; + + nr = (struct revlist *) xmalloc (sizeof *nr); + + if (r->first == NULL && r->last == NULL) + { + /* If both first and last are NULL, it means that we want + just the head of the default branch, which is RCS_head. */ + nr->first = RCS_head (rcs); + nr->last = xstrdup (nr->first); + nr->fields = numdots (nr->first) + 1; + } + else if (r->branchhead) + { + char *branch; + + /* Print just the head of the branch. */ + if (isdigit ((unsigned char) r->first[0])) + nr->first = RCS_getbranch (rcs, r->first, 1); + else + { + branch = RCS_whatbranch (rcs, r->first); + if (branch == NULL) + { + error (0, 0, "warning: `%s' is not a branch in `%s'", + r->first, rcs->path); + free (nr); + continue; + } + nr->first = RCS_getbranch (rcs, branch, 1); + free (branch); + } + if (nr->first == NULL) + { + error (0, 0, "warning: no revision `%s' in `%s'", + r->first, rcs->path); + free (nr); + continue; + } + nr->last = xstrdup (nr->first); + nr->fields = numdots (nr->first) + 1; + } + else + { + if (r->first == NULL || isdigit ((unsigned char) r->first[0])) + nr->first = xstrdup (r->first); + else + { + if (RCS_nodeisbranch (rcs, r->first)) + nr->first = RCS_whatbranch (rcs, r->first); + else + nr->first = RCS_gettag (rcs, r->first, 1, (int *) NULL); + if (nr->first == NULL) + { + error (0, 0, "warning: no revision `%s' in `%s'", + r->first, rcs->path); + free (nr); + continue; + } + } + + if (r->last == r->first) + nr->last = xstrdup (nr->first); + else if (r->last == NULL || isdigit ((unsigned char) r->last[0])) + nr->last = xstrdup (r->last); + else + { + if (RCS_nodeisbranch (rcs, r->last)) + nr->last = RCS_whatbranch (rcs, r->last); + else + nr->last = RCS_gettag (rcs, r->last, 1, (int *) NULL); + if (nr->last == NULL) + { + error (0, 0, "warning: no revision `%s' in `%s'", + r->last, rcs->path); + if (nr->first != NULL) + free (nr->first); + free (nr); + continue; + } + } + + /* Process the revision numbers the same way that rlog + does. This code is a bit cryptic for my tastes, but + keeping the same implementation as rlog ensures a + certain degree of compatibility. */ + if (r->first == NULL) + { + nr->fields = numdots (nr->last) + 1; + if (nr->fields < 2) + nr->first = xstrdup (".0"); + else + { + char *cp; + + nr->first = xstrdup (nr->last); + cp = strrchr (nr->first, '.'); + strcpy (cp, ".0"); + } + } + else if (r->last == NULL) + { + nr->fields = numdots (nr->first) + 1; + nr->last = xstrdup (nr->first); + if (nr->fields < 2) + nr->last[0] = '\0'; + else + { + char *cp; + + cp = strrchr (nr->last, '.'); + *cp = '\0'; + } + } + else + { + nr->fields = numdots (nr->first) + 1; + if (nr->fields != numdots (nr->last) + 1 + || (nr->fields > 2 + && version_compare (nr->first, nr->last, + nr->fields - 1) != 0)) + { + error (0, 0, + "invalid branch or revision pair %s:%s in `%s'", + r->first, r->last, rcs->path); + free (nr->first); + free (nr->last); + free (nr); + continue; + } + if (version_compare (nr->first, nr->last, nr->fields) > 0) + { + char *tmp; + + tmp = nr->first; + nr->first = nr->last; + nr->last = tmp; + } + } + } + + nr->next = NULL; + *pr = nr; + pr = &nr->next; + } + + /* If the default branch was requested, add a revlist entry for + it. This is how rlog handles this option. */ + if (default_branch + && (rcs->head != NULL || rcs->branch != NULL)) + { + struct revlist *nr; + + nr = (struct revlist *) xmalloc (sizeof *nr); + if (rcs->branch != NULL) + nr->first = xstrdup (rcs->branch); + else + { + char *cp; + + nr->first = xstrdup (rcs->head); + cp = strrchr (nr->first, '.'); + *cp = '\0'; + } + nr->last = xstrdup (nr->first); + nr->fields = numdots (nr->first) + 1; + + nr->next = NULL; + *pr = nr; + } + + return ret; +} + +/* + * Free a revlist created by log_expand_revlist. + */ +static void +log_free_revlist (revlist) + struct revlist *revlist; +{ + struct revlist *r; + + r = revlist; + while (r != NULL) + { + struct revlist *next; + + if (r->first != NULL) + free (r->first); + if (r->last != NULL) + free (r->last); + next = r->next; + free (r); + r = next; + } +} + +/* + * Return nonzero if a revision should be printed, based on the + * options provided. + */ +static int +log_version_requested (log_data, revlist, rcs, vnode) + struct log_data *log_data; + struct revlist *revlist; + RCSNode *rcs; + RCSVers *vnode; +{ + /* Handle the list of states from the -s option. */ + if (log_data->statelist != NULL + && findnode (log_data->statelist, vnode->state) == NULL) + { + return 0; + } + + /* Handle the list of authors from the -w option. */ + if (log_data->authorlist != NULL) + { + if (vnode->author != NULL + && findnode (log_data->authorlist, vnode->author) == NULL) + { + return 0; + } + } + + /* rlog considers all the -d options together when it decides + whether to print a revision, so we must be compatible. */ + if (log_data->datelist != NULL || log_data->singledatelist != NULL) + { + struct datelist *d; + + for (d = log_data->datelist; d != NULL; d = d->next) + { + int cmp; + + cmp = RCS_datecmp (vnode->date, d->start); + if (cmp > 0 || (cmp == 0 && d->inclusive)) + { + cmp = RCS_datecmp (vnode->date, d->end); + if (cmp < 0 || (cmp == 0 && d->inclusive)) + break; + } + } + + if (d == NULL) + { + /* Look through the list of specific dates. We want to + select the revision with the exact date found in the + start field. The commit code ensures that it is + impossible to check in multiple revisions of a single + file in a single second, so checking the date this way + should never select more than one revision. */ + for (d = log_data->singledatelist; d != NULL; d = d->next) + { + if (d->start != NULL + && RCS_datecmp (vnode->date, d->start) == 0) + { + break; + } + } + + if (d == NULL) + return 0; + } + } + + /* If the -r or -b options were used, REVLIST will be non NULL, + and we print the union of the specified revisions. */ + if (revlist != NULL) + { + char *v; + int vfields; + struct revlist *r; + + /* This code is taken from rlog. */ + v = vnode->version; + vfields = numdots (v) + 1; + for (r = revlist; r != NULL; r = r->next) + { + if (vfields == r->fields + (r->fields & 1) + && version_compare (v, r->first, r->fields) >= 0 + && version_compare (v, r->last, r->fields) <= 0) + { + return 1; + } + } + + /* If we get here, then the -b and/or the -r option was used, + but did not match this revision, so we reject it. */ + + return 0; + } + + /* By default, we print all revisions. */ + return 1; +} + +/* + * Output a single symbol. This is called via walklist. + */ +/*ARGSUSED*/ +static int +log_symbol (p, closure) + Node *p; + void *closure; +{ + cvs_output ("\n\t", 2); + cvs_output (p->key, 0); + cvs_output (": ", 2); + cvs_output (p->data, 0); + return 0; +} + +/* + * Count the number of entries on a list. This is called via walklist. + */ +/*ARGSUSED*/ +static int +log_count (p, closure) + Node *p; + void *closure; +{ + return 1; +} + +/* + * Sort out a single date specification by narrowing down the date + * until we find the specific selected revision. + */ +static int +log_fix_singledate (p, closure) + Node *p; + void *closure; +{ + struct log_data_and_rcs *data = (struct log_data_and_rcs *) closure; + Node *pv; + RCSVers *vnode; + struct datelist *holdsingle, *holddate; + int requested; + + pv = findnode (data->rcs->versions, p->key); + if (pv == NULL) + error (1, 0, "missing version `%s' in RCS file `%s'", + p->key, data->rcs->path); + vnode = (RCSVers *) pv->data; + + /* We are only interested if this revision passes any other tests. + Temporarily clear log_data->singledatelist to avoid confusing + log_version_requested. We also clear log_data->datelist, + because rlog considers all the -d options together. We don't + want to reject a revision because it does not match a date pair + if we are going to select it on the basis of the singledate. */ + holdsingle = data->log_data->singledatelist; + data->log_data->singledatelist = NULL; + holddate = data->log_data->datelist; + data->log_data->datelist = NULL; + requested = log_version_requested (data->log_data, data->revlist, + data->rcs, vnode); + data->log_data->singledatelist = holdsingle; + data->log_data->datelist = holddate; + + if (requested) + { + struct datelist *d; + + /* For each single date, if this revision is before the + specified date, but is closer than the previously selected + revision, select it instead. */ + for (d = data->log_data->singledatelist; d != NULL; d = d->next) + { + if (RCS_datecmp (vnode->date, d->end) <= 0 + && (d->start == NULL + || RCS_datecmp (vnode->date, d->start) > 0)) + { + if (d->start != NULL) + free (d->start); + d->start = xstrdup (vnode->date); + } + } + } + + return 0; +} + +/* + * Count the number of revisions we are going to print. + */ +static int +log_count_print (p, closure) + Node *p; + void *closure; +{ + struct log_data_and_rcs *data = (struct log_data_and_rcs *) closure; + Node *pv; + + pv = findnode (data->rcs->versions, p->key); + if (pv == NULL) + error (1, 0, "missing version `%s' in RCS file `%s'", + p->key, data->rcs->path); + if (log_version_requested (data->log_data, data->revlist, data->rcs, + (RCSVers *) pv->data)) + return 1; + else + return 0; +} + +/* + * Print the list of changes, not including the trunk, in reverse + * order for each branch. + */ +static void +log_tree (log_data, revlist, rcs, ver) + struct log_data *log_data; + struct revlist *revlist; + RCSNode *rcs; + const char *ver; +{ + Node *p; + RCSVers *vnode; + + p = findnode (rcs->versions, ver); + if (p == NULL) + error (1, 0, "missing version `%s' in RCS file `%s'", + ver, rcs->path); + vnode = (RCSVers *) p->data; + if (vnode->next != NULL) + log_tree (log_data, revlist, rcs, vnode->next); + if (vnode->branches != NULL) + { + Node *head, *branch; + + /* We need to do the branches in reverse order. This breaks + the List abstraction, but so does most of the branch + manipulation in rcs.c. */ + head = vnode->branches->list; + for (branch = head->prev; branch != head; branch = branch->prev) + { + log_abranch (log_data, revlist, rcs, branch->key); + log_tree (log_data, revlist, rcs, branch->key); + } + } +} + +/* + * Log the changes for a branch, in reverse order. + */ +static void +log_abranch (log_data, revlist, rcs, ver) + struct log_data *log_data; + struct revlist *revlist; + RCSNode *rcs; + const char *ver; +{ + Node *p; + RCSVers *vnode; + + p = findnode (rcs->versions, ver); + if (p == NULL) + error (1, 0, "missing version `%s' in RCS file `%s'", + ver, rcs->path); + vnode = (RCSVers *) p->data; + if (vnode->next != NULL) + log_abranch (log_data, revlist, rcs, vnode->next); + log_version (log_data, revlist, rcs, vnode, 0); +} + +/* + * Print the log output for a single version. + */ +static void +log_version (log_data, revlist, rcs, ver, trunk) + struct log_data *log_data; + struct revlist *revlist; + RCSNode *rcs; + RCSVers *ver; + int trunk; +{ + Node *p; + int year, mon, mday, hour, min, sec; + char buf[100]; + Node *padd, *pdel; + + if (! log_version_requested (log_data, revlist, rcs, ver)) + return; + + cvs_output ("----------------------------\nrevision ", 0); + cvs_output (ver->version, 0); + + p = findnode (RCS_getlocks (rcs), ver->version); + if (p != NULL) + { + cvs_output ("\tlocked by: ", 0); + cvs_output (p->data, 0); + cvs_output (";", 1); + } + + cvs_output ("\ndate: ", 0); + (void) sscanf (ver->date, SDATEFORM, &year, &mon, &mday, &hour, &min, + &sec); + if (year < 1900) + year += 1900; + sprintf (buf, "%04d/%02d/%02d %02d:%02d:%02d", year, mon, mday, + hour, min, sec); + cvs_output (buf, 0); + + cvs_output ("; author: ", 0); + cvs_output (ver->author, 0); + + cvs_output ("; state: ", 0); + cvs_output (ver->state, 0); + cvs_output (";", 1); + + if (! trunk) + { + padd = findnode (ver->other, ";add"); + pdel = findnode (ver->other, ";delete"); + } + else if (ver->next == NULL) + { + padd = NULL; + pdel = NULL; + } + else + { + Node *nextp; + RCSVers *nextver; + + nextp = findnode (rcs->versions, ver->next); + if (nextp == NULL) + error (1, 0, "missing version `%s' in `%s'", ver->next, + rcs->path); + nextver = (RCSVers *) nextp->data; + pdel = findnode (nextver->other, ";add"); + padd = findnode (nextver->other, ";delete"); + } + + if (padd != NULL) + { + cvs_output (" lines: +", 0); + cvs_output (padd->data, 0); + cvs_output (" -", 2); + cvs_output (pdel->data, 0); + } + + if (ver->branches != NULL) + { + cvs_output ("\nbranches:", 0); + walklist (ver->branches, log_branch, (void *) NULL); + } + + cvs_output ("\n", 1); + + p = findnode (ver->other, "log"); + /* The p->date == NULL case is the normal one for an empty log + message (rcs-14 in sanity.sh). I don't think the case where + p->data is "" can happen (getrcskey in rcs.c checks for an + empty string and set the value to NULL in that case). My guess + would be the p == NULL case would mean an RCS file which was + missing the "log" keyword (which is illegal according to + rcsfile.5). */ + if (p == NULL || p->data == NULL || p->data[0] == '\0') + cvs_output ("*** empty log message ***\n", 0); + else + { + /* FIXME: Technically, the log message could contain a null + byte. */ + cvs_output (p->data, 0); + if (p->data[strlen (p->data) - 1] != '\n') + cvs_output ("\n", 1); + } +} + +/* + * Output a branch version. This is called via walklist. + */ +/*ARGSUSED*/ +static int +log_branch (p, closure) + Node *p; + void *closure; +{ + cvs_output (" ", 2); + if ((numdots (p->key) & 1) == 0) + cvs_output (p->key, 0); + else + { + char *f, *cp; + + f = xstrdup (p->key); + cp = strrchr (f, '.'); + *cp = '\0'; + cvs_output (f, 0); + free (f); + } + cvs_output (";", 1); + return 0; +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +log_dirproc (callerdat, dir, repository, update_dir, entries) + void *callerdat; + char *dir; + char *repository; + char *update_dir; + List *entries; +{ + if (!isdir (dir)) + return (R_SKIP_ALL); + + if (!quiet) + error (0, 0, "Logging %s", update_dir); + return (R_PROCESS); +} + +/* + * Compare versions. This is taken from RCS compartial. + */ +static int +version_compare (v1, v2, len) + const char *v1; + const char *v2; + int len; +{ + while (1) + { + int d1, d2, r; + + if (*v1 == '\0') + return 1; + if (*v2 == '\0') + return -1; + + while (*v1 == '0') + ++v1; + for (d1 = 0; isdigit ((unsigned char) v1[d1]); ++d1) + ; + + while (*v2 == '0') + ++v2; + for (d2 = 0; isdigit ((unsigned char) v2[d2]); ++d2) + ; + + if (d1 != d2) + return d1 < d2 ? -1 : 1; + + r = memcmp (v1, v2, d1); + if (r != 0) + return r; + + --len; + if (len == 0) + return 0; + + v1 += d1; + v2 += d1; + + if (*v1 == '.') + ++v1; + if (*v2 == '.') + ++v2; + } +} diff --git a/gnu/dist/cvs/src/login.c b/gnu/dist/cvs/src/login.c new file mode 100644 index 00000000000..2bbf5b95ebf --- /dev/null +++ b/gnu/dist/cvs/src/login.c @@ -0,0 +1,502 @@ +/* + * Copyright (c) 1995, Cyclic Software, Bloomington, IN, USA + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with CVS. + * + * Allow user to log in for an authenticating server. + */ + +#include "cvs.h" +#include "getline.h" + +#ifdef AUTH_CLIENT_SUPPORT /* This covers the rest of the file. */ + +/* There seems to be very little agreement on which system header + getpass is declared in. With a lot of fancy autoconfiscation, + we could perhaps detect this, but for now we'll just rely on + _CRAY, since Cray is perhaps the only system on which our own + declaration won't work (some Crays declare the 2#$@% thing as + varadic, believe it or not). On Cray, getpass will be declared + in either stdlib.h or unistd.h. */ +#ifndef _CRAY +extern char *getpass (); +#endif + +#ifndef CVS_PASSWORD_FILE +#define CVS_PASSWORD_FILE ".cvspass" +#endif + +/* If non-NULL, get_cvs_password() will just return this. */ +static char *cvs_password = NULL; + +static char *construct_cvspass_filename PROTO ((void)); + +/* The return value will need to be freed. */ +static char * +construct_cvspass_filename () +{ + char *homedir; + char *passfile; + + /* Environment should override file. */ + if ((passfile = getenv ("CVS_PASSFILE")) != NULL) + return xstrdup (passfile); + + /* Construct absolute pathname to user's password file. */ + /* todo: does this work under OS/2 ? */ + homedir = get_homedir (); + if (! homedir) + { + /* FIXME? This message confuses a lot of users, at least + on Win95 (which doesn't set HOMEDRIVE and HOMEPATH like + NT does). I suppose the answer for Win95 is to store the + passwords in the registry or something (??). And .cvsrc + and such too? Wonder what WinCVS does (about .cvsrc, the + right thing for a GUI is to just store the password in + memory only)... */ + error (1, 0, "could not find out home directory"); + return (char *) NULL; + } + + passfile = + (char *) xmalloc (strlen (homedir) + strlen (CVS_PASSWORD_FILE) + 3); + strcpy (passfile, homedir); +#ifndef NO_SLASH_AFTER_HOME + /* NO_SLASH_AFTER_HOME is defined for VMS, where foo:[bar]/.cvspass is not + a legal filename but foo:[bar].cvspass is. A more clean solution would + be something more along the lines of a "join a directory to a filename" + kind of thing.... */ + strcat (passfile, "/"); +#endif + strcat (passfile, CVS_PASSWORD_FILE); + + /* Safety first and last, Scouts. */ + if (isfile (passfile)) + /* xchmod() is too polite. */ + chmod (passfile, 0600); + + return passfile; +} + +static const char *const login_usage[] = +{ + "Usage: %s %s\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* Prompt for a password, and store it in the file "CVS/.cvspass". + * + * Because the user might be accessing multiple repositories, with + * different passwords for each one, the format of ~/.cvspass is: + * + * user@host:/path Acleartext_password + * user@host:/path Acleartext_password + * ... + * + * Of course, the "user@" might be left off -- it's just based on the + * value of CVSroot. + * + * The "A" before "cleartext_password" is a literal capital A. It's a + * version number indicating which form of scrambling we're doing on + * the password -- someday we might provide something more secure than + * the trivial encoding we do now, and when that day comes, it would + * be nice to remain backward-compatible. + * + * Like .netrc, the file's permissions are the only thing preventing + * it from being read by others. Unlike .netrc, we will not be + * fascist about it, at most issuing a warning, and never refusing to + * work. + */ +int +login (argc, argv) + int argc; + char **argv; +{ + char *passfile; + FILE *fp; + char *typed_password, *found_password; + char *linebuf = (char *) NULL; + size_t linebuf_len; + int root_len, already_entered = 0; + int line_length; + + if (argc < 0) + usage (login_usage); + + if (CVSroot_method != pserver_method) + { + error (0, 0, "can only use pserver method with `login' command"); + error (1, 0, "CVSROOT: %s", CVSroot_original); + } + + if (! CVSroot_username) + { + error (0, 0, "CVSROOT \"%s\" is not fully-qualified.", + CVSroot_original); + error (1, 0, "Please make sure to specify \"user@host\"!"); + } + + printf ("(Logging in to %s@%s)\n", CVSroot_username, CVSroot_hostname); + fflush (stdout); + + passfile = construct_cvspass_filename (); + typed_password = getpass ("CVS password: "); + typed_password = scramble (typed_password); + + /* Force get_cvs_password() to use this one (when the client + * confirms the new password with the server), instead of + * consulting the file. We make a new copy because cvs_password + * will get zeroed by connect_to_server(). */ + + cvs_password = xstrdup (typed_password); + + connect_to_pserver (NULL, NULL, 1, 0); + + /* IF we have a password for this "[user@]host:/path" already + * THEN + * IF it's the same as the password we read from the prompt + * THEN + * do nothing + * ELSE + * replace the old password with the new one + * ELSE + * append new entry to the end of the file. + */ + + root_len = strlen (CVSroot_original); + + /* Yes, the method below reads the user's password file twice. It's + inefficient, but we're not talking about a gig of data here. */ + + fp = CVS_FOPEN (passfile, "r"); + /* FIXME: should be printing a message if fp == NULL and not + existence_error (errno). */ + if (fp != NULL) + { + /* Check each line to see if we have this entry already. */ + while ((line_length = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + if (strncmp (CVSroot_original, linebuf, root_len) == 0) + { + already_entered = 1; + break; + } + } + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", passfile); + } + else if (!existence_error (errno)) + error (0, errno, "cannot open %s", passfile); + + if (already_entered) + { + /* This user/host has a password in the file already. */ + + strtok (linebuf, " "); + found_password = strtok (NULL, "\n"); + if (strcmp (found_password, typed_password)) + { + /* typed_password and found_password don't match, so we'll + * have to update passfile. We replace the old password + * with the new one by writing a tmp file whose contents are + * exactly the same as passfile except that this one entry + * gets typed_password instead of found_password. Then we + * rename the tmp file on top of passfile. + */ + char *tmp_name; + FILE *tmp_fp; + + tmp_name = cvs_temp_name (); + if ((tmp_fp = CVS_FOPEN (tmp_name, "w")) == NULL) + { + error (1, errno, "unable to open temp file %s", tmp_name); + return 1; + } + chmod (tmp_name, 0600); + + fp = CVS_FOPEN (passfile, "r"); + if (fp == NULL) + { + error (1, errno, "unable to open %s", passfile); + if (linebuf) + free (linebuf); + return 1; + } + /* I'm not paranoid, they really ARE out to get me: */ + chmod (passfile, 0600); + + while ((line_length = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + if (strncmp (CVSroot_original, linebuf, root_len)) + { + if (fprintf (tmp_fp, "%s", linebuf) == EOF) + error (0, errno, "cannot write %s", tmp_name); + } + else + { + if (fprintf (tmp_fp, "%s %s\n", CVSroot_original, + typed_password) == EOF) + error (0, errno, "cannot write %s", tmp_name); + } + } + if (line_length < 0 && !feof (fp)) + error (0, errno, "cannot read %s", passfile); + if (linebuf) + free (linebuf); + if (fclose (tmp_fp) < 0) + error (0, errno, "cannot close %s", tmp_name); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", passfile); + + /* FIXME: rename_file would make more sense (e.g. almost + always faster). */ + copy_file (tmp_name, passfile); + if (unlink_file (tmp_name) < 0) + error (0, errno, "cannot remove %s", tmp_name); + chmod (passfile, 0600); + + free (tmp_name); + } + } + else + { + if (linebuf) + free (linebuf); + if ((fp = CVS_FOPEN (passfile, "a")) == NULL) + { + error (1, errno, "could not open %s", passfile); + free (passfile); + return 1; + } + + if (fprintf (fp, "%s %s\n", CVSroot_original, typed_password) == EOF) + error (0, errno, "cannot write %s", passfile); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", passfile); + } + + /* Utter, total, raving paranoia, I know. */ + chmod (passfile, 0600); + memset (typed_password, 0, strlen (typed_password)); + free (typed_password); + + free (passfile); + free (cvs_password); + cvs_password = NULL; + return 0; +} + +/* Returns the _scrambled_ password. The server must descramble + before hashing and comparing. If password file not found, or + password not found in the file, just return NULL. */ +char * +get_cvs_password () +{ + int found_it = 0; + int root_len; + char *password = NULL; + char *linebuf = NULL; + size_t linebuf_len; + FILE *fp; + char *passfile; + int line_length; + + /* If someone (i.e., login()) is calling connect_to_pserver() out of + context, then assume they have supplied the correct, scrambled + password. */ + if (cvs_password) + return cvs_password; + + if (getenv ("CVS_PASSWORD") != NULL) + { + /* In previous versions of CVS one could specify a password in + CVS_PASSWORD. This is a bad idea, because in BSD variants + of unix anyone can see the environment variable with 'ps'. + But for users who were using that feature we want to at + least let them know what is going on. After printing this + warning, we should fall through to the regular error where + we tell them to run "cvs login" (unless they already ran + it, of course). */ + error (0, 0, "CVS_PASSWORD is no longer supported; ignored"); + } + + /* Else get it from the file. First make sure that the CVSROOT + variable has the appropriate fields filled in. */ + + if (CVSroot_method != pserver_method) + { + error (0, 0, "can only call GET_CVS_PASSWORD with pserver method"); + error (1, 0, "CVSROOT: %s", CVSroot_original); + } + + if (! CVSroot_username) + { + error (0, 0, "CVSROOT \"%s\" is not fully-qualified.", + CVSroot_original); + error (1, 0, "Please make sure to specify \"user@host\"!"); + } + + passfile = construct_cvspass_filename (); + fp = CVS_FOPEN (passfile, "r"); + if (fp == NULL) + { + free (passfile); + return NULL; + } + + root_len = strlen (CVSroot_original); + + /* Check each line to see if we have this entry already. */ + while ((line_length = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + if (strncmp (CVSroot_original, linebuf, root_len) == 0) + { + /* This is it! So break out and deal with linebuf. */ + found_it = 1; + break; + } + } + if (line_length < 0 && !feof (fp)) + error (0, errno, "cannot read %s", passfile); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", passfile); + + if (found_it) + { + /* linebuf now contains the line with the password. */ + char *tmp; + + strtok (linebuf, " "); + tmp = strtok (NULL, "\n"); + + /* Give it permanent storage. */ + password = xstrdup (tmp); + memset (tmp, 0, strlen (password)); + } + + if (linebuf) + free (linebuf); + free (passfile); + return password; +} + +static const char *const logout_usage[] = +{ + "Usage: %s %s\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* Remove any entry for the CVSRoot repository found in .cvspass. */ +int +logout (argc, argv) + int argc; + char **argv; +{ + char *passfile; + FILE *fp; + char *tmp_name = NULL; + FILE *tmp_fp; + char *linebuf = (char *) NULL; + size_t linebuf_len; + int root_len, found = 0; + int line_length; + + if (argc < 0) + usage (logout_usage); + + if (CVSroot_method != pserver_method) + { + error (0, 0, "can only use pserver method with `logout' command"); + error (1, 0, "CVSROOT: %s", CVSroot_original); + } + + if (! CVSroot_username) + { + error (0, 0, "CVSROOT \"%s\" is not fully-qualified.", + CVSroot_original); + error (1, 0, "Please make sure to specify \"user@host\"!"); + } + + /* Hmm. Do we want a variant of this command which deletes _all_ + the entries from the current .cvspass? Might be easier to + remember than "rm ~/.cvspass" but then again if people are + mucking with HOME (common in Win95 as the system doesn't set + it), then this variant of "cvs logout" might give a false sense + of security, in that it wouldn't delete entries from any + .cvspass files but the current one. */ + + printf ("(Logging out of %s@%s)\n", CVSroot_username, CVSroot_hostname); + fflush (stdout); + + /* IF we have a password for this "[user@]host:/path" already + * THEN + * drop the entry + * ELSE + * do nothing + */ + + passfile = construct_cvspass_filename (); + /* FIXME: This should not be in /tmp; that is almost surely a security + hole. Probably should just keep it in memory. */ + tmp_name = cvs_temp_name (); + if ((tmp_fp = CVS_FOPEN (tmp_name, "w")) == NULL) + { + error (1, errno, "unable to open temp file %s", tmp_name); + return 1; + } + chmod (tmp_name, 0600); + + root_len = strlen (CVSroot_original); + + fp = CVS_FOPEN (passfile, "r"); + if (fp == NULL) + error (1, errno, "Error opening %s", passfile); + + /* Check each line to see if we have this entry. */ + /* Copy only those lines that do not match this entry */ + while ((line_length = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + if (strncmp (CVSroot_original, linebuf, root_len)) + { + if (fprintf (tmp_fp, "%s", linebuf) == EOF) + error (0, errno, "cannot write %s", tmp_name); + } + else + found = 1; + } + if (line_length < 0 && !feof (fp)) + error (0, errno, "cannot read %s", passfile); + + if (linebuf) + free (linebuf); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", passfile); + if (fclose (tmp_fp) < 0) + error (0, errno, "cannot close %s", tmp_name); + + if (! found) + { + printf ("Entry not found for %s\n", CVSroot_original); + if (unlink_file (tmp_name) < 0) + error (0, errno, "cannot remove %s", tmp_name); + } + else + { + /* FIXME: rename_file would make more sense (e.g. almost + always faster). */ + copy_file (tmp_name, passfile); + if (unlink_file (tmp_name) < 0) + error (0, errno, "cannot remove %s", tmp_name); + chmod (passfile, 0600); + } + + if (tmp_name) + free (tmp_name); + + return 0; +} + +#endif /* AUTH_CLIENT_SUPPORT from beginning of file. */ diff --git a/gnu/dist/cvs/src/logmsg.c b/gnu/dist/cvs/src/logmsg.c new file mode 100644 index 00000000000..50ced198ba9 --- /dev/null +++ b/gnu/dist/cvs/src/logmsg.c @@ -0,0 +1,886 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + */ + +#include "cvs.h" +#include "getline.h" + +static int find_type PROTO((Node * p, void *closure)); +static int fmt_proc PROTO((Node * p, void *closure)); +static int logfile_write PROTO((char *repository, char *filter, + char *message, FILE * logfp, List * changes)); +static int rcsinfo_proc PROTO((char *repository, char *template)); +static int title_proc PROTO((Node * p, void *closure)); +static int update_logfile_proc PROTO((char *repository, char *filter)); +static void setup_tmpfile PROTO((FILE * xfp, char *xprefix, List * changes)); +static int editinfo_proc PROTO((char *repository, char *template)); +static int verifymsg_proc PROTO((char *repository, char *script)); + +static FILE *fp; +static char *str_list; +static char *str_list_format; /* The format for str_list's contents. */ +static char *editinfo_editor; +static char *verifymsg_script; +static Ctype type; + +/* + * Puts a standard header on the output which is either being prepared for an + * editor session, or being sent to a logfile program. The modified, added, + * and removed files are included (if any) and formatted to look pretty. */ +static char *prefix; +static int col; +static char *tag; +static void +setup_tmpfile (xfp, xprefix, changes) + FILE *xfp; + char *xprefix; + List *changes; +{ + /* set up statics */ + fp = xfp; + prefix = xprefix; + + type = T_MODIFIED; + if (walklist (changes, find_type, NULL) != 0) + { + (void) fprintf (fp, "%sModified Files:\n", prefix); + col = 0; + (void) walklist (changes, fmt_proc, NULL); + (void) fprintf (fp, "\n"); + if (tag != NULL) + { + free (tag); + tag = NULL; + } + } + type = T_ADDED; + if (walklist (changes, find_type, NULL) != 0) + { + (void) fprintf (fp, "%sAdded Files:\n", prefix); + col = 0; + (void) walklist (changes, fmt_proc, NULL); + (void) fprintf (fp, "\n"); + if (tag != NULL) + { + free (tag); + tag = NULL; + } + } + type = T_REMOVED; + if (walklist (changes, find_type, NULL) != 0) + { + (void) fprintf (fp, "%sRemoved Files:\n", prefix); + col = 0; + (void) walklist (changes, fmt_proc, NULL); + (void) fprintf (fp, "\n"); + if (tag != NULL) + { + free (tag); + tag = NULL; + } + } +} + +/* + * Looks for nodes of a specified type and returns 1 if found + */ +static int +find_type (p, closure) + Node *p; + void *closure; +{ + struct logfile_info *li; + + li = (struct logfile_info *) p->data; + if (li->type == type) + return (1); + else + return (0); +} + +/* + * Breaks the files list into reasonable sized lines to avoid line wrap... + * all in the name of pretty output. It only works on nodes whose types + * match the one we're looking for + */ +static int +fmt_proc (p, closure) + Node *p; + void *closure; +{ + struct logfile_info *li; + + li = (struct logfile_info *) p->data; + if (li->type == type) + { + if (li->tag == NULL + ? tag != NULL + : tag == NULL || strcmp (tag, li->tag) != 0) + { + if (col > 0) + (void) fprintf (fp, "\n"); + (void) fprintf (fp, "%s", prefix); + col = strlen (prefix); + while (col < 6) + { + (void) fprintf (fp, " "); + ++col; + } + + if (li->tag == NULL) + (void) fprintf (fp, "No tag"); + else + (void) fprintf (fp, "Tag: %s", li->tag); + + if (tag != NULL) + free (tag); + tag = xstrdup (li->tag); + + /* Force a new line. */ + col = 70; + } + + if (col == 0) + { + (void) fprintf (fp, "%s\t", prefix); + col = 8; + } + else if (col > 8 && (col + (int) strlen (p->key)) > 70) + { + (void) fprintf (fp, "\n%s\t", prefix); + col = 8; + } + (void) fprintf (fp, "%s ", p->key); + col += strlen (p->key) + 1; + } + return (0); +} + +/* + * Builds a temporary file using setup_tmpfile() and invokes the user's + * editor on the file. The header garbage in the resultant file is then + * stripped and the log message is stored in the "message" argument. + * + * If REPOSITORY is non-NULL, process rcsinfo for that repository; if it + * is NULL, use the CVSADM_TEMPLATE file instead. + */ +void +do_editor (dir, messagep, repository, changes) + char *dir; + char **messagep; + char *repository; + List *changes; +{ + static int reuse_log_message = 0; + char *line; + int line_length; + size_t line_chars_allocated; + char *fname; + struct stat pre_stbuf, post_stbuf; + int retcode = 0; + char *p; + + if (noexec || reuse_log_message) + return; + + /* Abort creation of temp file if no editor is defined */ + if (strcmp (Editor, "") == 0 && !editinfo_editor) + error(1, 0, "no editor defined, must use -e or -m"); + + + /* Create a temporary file */ + fname = cvs_temp_name (); + again: + if ((fp = CVS_FOPEN (fname, "w+")) == NULL) + error (1, 0, "cannot create temporary file %s", fname); + + if (*messagep) + { + (void) fprintf (fp, "%s", *messagep); + + if ((*messagep)[0] == '\0' || + (*messagep)[strlen (*messagep) - 1] != '\n') + (void) fprintf (fp, "\n"); + } + else + (void) fprintf (fp, "\n"); + + if (repository != NULL) + /* tack templates on if necessary */ + (void) Parse_Info (CVSROOTADM_RCSINFO, repository, rcsinfo_proc, 1); + else + { + FILE *tfp; + char buf[1024]; + char *p; + size_t n; + size_t nwrite; + + /* Why "b"? */ + tfp = CVS_FOPEN (CVSADM_TEMPLATE, "rb"); + if (tfp == NULL) + { + if (!existence_error (errno)) + error (1, errno, "cannot read %s", CVSADM_TEMPLATE); + } + else + { + while (!feof (tfp)) + { + n = fread (buf, 1, sizeof buf, tfp); + nwrite = n; + p = buf; + while (nwrite > 0) + { + n = fwrite (p, 1, nwrite, fp); + nwrite -= n; + p += n; + } + if (ferror (tfp)) + error (1, errno, "cannot read %s", CVSADM_TEMPLATE); + } + if (fclose (tfp) < 0) + error (0, errno, "cannot close %s", CVSADM_TEMPLATE); + } + } + + (void) fprintf (fp, + "%s----------------------------------------------------------------------\n", + CVSEDITPREFIX); + (void) fprintf (fp, + "%sEnter Log. Lines beginning with `%.*s' are removed automatically\n%s\n", + CVSEDITPREFIX, CVSEDITPREFIXLEN, CVSEDITPREFIX, + CVSEDITPREFIX); + if (dir != NULL && *dir) + (void) fprintf (fp, "%sCommitting in %s\n%s\n", CVSEDITPREFIX, + dir, CVSEDITPREFIX); + if (changes != NULL) + setup_tmpfile (fp, CVSEDITPREFIX, changes); + (void) fprintf (fp, + "%s----------------------------------------------------------------------\n", + CVSEDITPREFIX); + + /* finish off the temp file */ + if (fclose (fp) == EOF) + error (1, errno, "%s", fname); + if ( CVS_STAT (fname, &pre_stbuf) == -1) + pre_stbuf.st_mtime = 0; + + if (editinfo_editor) + free (editinfo_editor); + editinfo_editor = (char *) NULL; +#ifdef CLIENT_SUPPORT + if (client_active) + ; /* nothing, leave editinfo_editor NULL */ + else +#endif + if (repository != NULL) + (void) Parse_Info (CVSROOTADM_EDITINFO, repository, editinfo_proc, 0); + + /* run the editor */ + run_setup (editinfo_editor ? editinfo_editor : Editor); + run_arg (fname); + if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY, + RUN_NORMAL | RUN_SIGIGNORE)) != 0) + error (editinfo_editor ? 1 : 0, retcode == -1 ? errno : 0, + editinfo_editor ? "Logfile verification failed" : + "warning: editor session failed"); + + /* put the entire message back into the *messagep variable */ + + fp = open_file (fname, "r"); + + if (*messagep) + free (*messagep); + + if ( CVS_STAT (fname, &post_stbuf) != 0) + error (1, errno, "cannot find size of temp file %s", fname); + + if (post_stbuf.st_size == 0) + *messagep = NULL; + else + { + /* On NT, we might read less than st_size bytes, but we won't + read more. So this works. */ + *messagep = (char *) xmalloc (post_stbuf.st_size + 1); + *messagep[0] = '\0'; + } + + line = NULL; + line_chars_allocated = 0; + + if (*messagep) + { + p = *messagep; + while (1) + { + line_length = getline (&line, &line_chars_allocated, fp); + if (line_length == -1) + { + if (ferror (fp)) + error (0, errno, "warning: cannot read %s", fname); + break; + } + if (strncmp (line, CVSEDITPREFIX, CVSEDITPREFIXLEN) == 0) + continue; + (void) strcpy (p, line); + p += line_length; + } + } + if (fclose (fp) < 0) + error (0, errno, "warning: cannot close %s", fname); + + if (pre_stbuf.st_mtime == post_stbuf.st_mtime || + *messagep == NULL || + strcmp (*messagep, "\n") == 0) + { + for (;;) + { + (void) printf ("\nLog message unchanged or not specified\n"); + (void) printf ("a)bort, c)ontinue, e)dit, !)reuse this message unchanged for remaining dirs\n"); + (void) printf ("Action: (continue) "); + (void) fflush (stdout); + line_length = getline (&line, &line_chars_allocated, stdin); + if (line_length < 0) + { + error (0, errno, "cannot read from stdin"); + if (unlink_file (fname) < 0) + error (0, errno, + "warning: cannot remove temp file %s", fname); + error (1, 0, "aborting"); + } + else if (line_length == 0 + || *line == '\n' || *line == 'c' || *line == 'C') + break; + if (*line == 'a' || *line == 'A') + { + if (unlink_file (fname) < 0) + error (0, errno, "warning: cannot remove temp file %s", fname); + error (1, 0, "aborted by user"); + } + if (*line == 'e' || *line == 'E') + goto again; + if (*line == '!') + { + reuse_log_message = 1; + break; + } + (void) printf ("Unknown input\n"); + } + } + if (line) + free (line); + if (unlink_file (fname) < 0) + error (0, errno, "warning: cannot remove temp file %s", fname); + free (fname); +} + +/* Runs the user-defined verification script as part of the commit or import + process. This verification is meant to be run whether or not the user + included the -m atribute. unlike the do_editor function, this is + independant of the running of an editor for getting a message. + */ +void +do_verify (message, repository) + char *message; + char *repository; +{ + FILE *fp; + char *fname; + int retcode = 0; + +#ifdef CLIENT_SUPPORT + if (client_active) + /* The verification will happen on the server. */ + return; +#endif + + /* FIXME? Do we really want to skip this on noexec? What do we do + for the other administrative files? */ + if (noexec) + return; + + /* If there's no message, then we have nothing to verify. Can this + case happen? And if so why would we print a message? */ + if (message == NULL) + { + cvs_output ("No message to verify\n", 0); + return; + } + + /* Get a temp filename, open a temporary file, write the message to the + temp file, and close the file. */ + + fname = cvs_temp_name (); + + fp = fopen (fname, "w"); + if (fp == NULL) + error (1, errno, "cannot create temporary file %s", fname); + else + { + fprintf (fp, "%s", message); + if ((message)[0] == '\0' || + (message)[strlen (message) - 1] != '\n') + (void) fprintf (fp, "%s", "\n"); + if (fclose (fp) == EOF) + error (1, errno, "%s", fname); + + /* Get the name of the verification script to run */ + + if (repository != NULL) + (void) Parse_Info (CVSROOTADM_VERIFYMSG, repository, + verifymsg_proc, 0); + + /* Run the verification script */ + + if (verifymsg_script) + { + run_setup (verifymsg_script); + run_arg (fname); + if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY, + RUN_NORMAL | RUN_SIGIGNORE)) != 0) + { + /* Since following error() exits, delete the temp file + now. */ + if (unlink_file (fname) < 0) + error (0, errno, "cannot remove %s", fname); + + error (1, retcode == -1 ? errno : 0, + "Message verification failed"); + } + } + + /* Delete the temp file */ + + if (unlink_file (fname) < 0) + error (0, errno, "cannot remove %s", fname); + free (fname); + } +} + +/* + * callback proc for Parse_Info for rcsinfo templates this routine basically + * copies the matching template onto the end of the tempfile we are setting + * up + */ +/* ARGSUSED */ +static int +rcsinfo_proc (repository, template) + char *repository; + char *template; +{ + static char *last_template; + FILE *tfp; + + /* nothing to do if the last one included is the same as this one */ + if (last_template && strcmp (last_template, template) == 0) + return (0); + if (last_template) + free (last_template); + last_template = xstrdup (template); + + if ((tfp = CVS_FOPEN (template, "r")) != NULL) + { + char *line = NULL; + size_t line_chars_allocated = 0; + + while (getline (&line, &line_chars_allocated, tfp) >= 0) + (void) fputs (line, fp); + if (ferror (tfp)) + error (0, errno, "warning: cannot read %s", template); + if (fclose (tfp) < 0) + error (0, errno, "warning: cannot close %s", template); + if (line) + free (line); + return (0); + } + else + { + error (0, errno, "Couldn't open rcsinfo template file %s", template); + return (1); + } +} + +/* + * Uses setup_tmpfile() to pass the updated message on directly to any + * logfile programs that have a regular expression match for the checked in + * directory in the source repository. The log information is fed into the + * specified program as standard input. + */ +static FILE *logfp; +static char *message; +static List *changes; + +void +Update_Logfile (repository, xmessage, xlogfp, xchanges) + char *repository; + char *xmessage; + FILE *xlogfp; + List *xchanges; +{ + /* nothing to do if the list is empty */ + if (xchanges == NULL || xchanges->list->next == xchanges->list) + return; + + /* set up static vars for update_logfile_proc */ + message = xmessage; + logfp = xlogfp; + changes = xchanges; + + /* call Parse_Info to do the actual logfile updates */ + (void) Parse_Info (CVSROOTADM_LOGINFO, repository, update_logfile_proc, 1); +} + +/* + * callback proc to actually do the logfile write from Update_Logfile + */ +static int +update_logfile_proc (repository, filter) + char *repository; + char *filter; +{ + return (logfile_write (repository, filter, message, logfp, changes)); +} + +/* + * concatenate each filename/version onto str_list + */ +static int +title_proc (p, closure) + Node *p; + void *closure; +{ + struct logfile_info *li; + char *c; + + li = (struct logfile_info *) p->data; + if (li->type == type) + { + /* Until we decide on the correct logging solution when we add + directories or perform imports, T_TITLE nodes will only + tack on the name provided, regardless of the format string. + You can verify that this assumption is safe by checking the + code in add.c (add_directory) and import.c (import). */ + + str_list = xrealloc (str_list, strlen (str_list) + 5); + (void) strcat (str_list, " "); + + if (li->type == T_TITLE) + { + str_list = xrealloc (str_list, + strlen (str_list) + strlen (p->key) + 5); + (void) strcat (str_list, p->key); + } + else + { + /* All other nodes use the format string. */ + + for (c = str_list_format; *c != '\0'; c++) + { + switch (*c) + { + case 's': + str_list = + xrealloc (str_list, + strlen (str_list) + strlen (p->key) + 5); + (void) strcat (str_list, p->key); + break; + case 'V': + str_list = + xrealloc (str_list, + (strlen (str_list) + + (li->rev_old ? strlen (li->rev_old) : 0) + + 10) + ); + (void) strcat (str_list, (li->rev_old + ? li->rev_old : "NONE")); + break; + case 'v': + str_list = + xrealloc (str_list, + (strlen (str_list) + + (li->rev_new ? strlen (li->rev_new) : 0) + + 10) + ); + (void) strcat (str_list, (li->rev_new + ? li->rev_new : "NONE")); + break; + /* All other characters, we insert an empty field (but + we do put in the comma separating it from other + fields). This way if future CVS versions add formatting + characters, one can write a loginfo file which at least + won't blow up on an old CVS. */ + } + if (*(c + 1) != '\0') + { + str_list = xrealloc (str_list, strlen (str_list) + 5); + (void) strcat (str_list, ","); + } + } + } + } + return (0); +} + +/* + * Writes some stuff to the logfile "filter" and returns the status of the + * filter program. + */ +static int +logfile_write (repository, filter, message, logfp, changes) + char *repository; + char *filter; + char *message; + FILE *logfp; + List *changes; +{ + FILE *pipefp; + char *prog; + char *cp; + int c; + int pipestatus; + char *fmt_percent; /* the location of the percent sign + that starts the format string. */ + + /* The user may specify a format string as part of the filter. + Originally, `%s' was the only valid string. The string that + was substituted for it was: + + <repository-name> <file1> <file2> <file3> ... + + Each file was either a new directory/import (T_TITLE), or a + added (T_ADDED), modified (T_MODIFIED), or removed (T_REMOVED) + file. + + It is desirable to preserve that behavior so lots of commitlog + scripts won't die when they get this new code. At the same + time, we'd like to pass other information about the files (like + version numbers, statuses, or checkin times). + + The solution is to allow a format string that allows us to + specify those other pieces of information. The format string + will be composed of `%' followed by a single format character, + or followed by a set of format characters surrounded by `{' and + `}' as separators. The format characters are: + + s = file name + V = old version number (pre-checkin) + v = new version number (post-checkin) + + For example, valid format strings are: + + %{} + %s + %{s} + %{sVv} + + There's no reason that more items couldn't be added (like + modification date or file status [added, modified, updated, + etc.]) -- the code modifications would be minimal (logmsg.c + (title_proc) and commit.c (check_fileproc)). + + The output will be a string of tokens separated by spaces. For + backwards compatibility, the the first token will be the + repository name. The rest of the tokens will be + comma-delimited lists of the information requested in the + format string. For example, if `/u/src/master' is the + repository, `%{sVv}' is the format string, and three files + (ChangeLog, Makefile, foo.c) were modified, the output might + be: + + /u/src/master ChangeLog,1.1,1.2 Makefile,1.3,1.4 foo.c,1.12,1.13 + + Why this duplicates the old behavior when the format string is + `%s' is left as an exercise for the reader. */ + + fmt_percent = strchr (filter, '%'); + if (fmt_percent) + { + int len; + char *srepos; + char *fmt_begin, *fmt_end; /* beginning and end of the + format string specified in + filter. */ + char *fmt_continue; /* where the string continues + after the format string (we + might skip a '}') somewhere + in there... */ + + /* Grab the format string. */ + + if ((*(fmt_percent + 1) == ' ') || (*(fmt_percent + 1) == '\0')) + { + /* The percent stands alone. This is an error. We could + be treating ' ' like any other formatting character, but + using it as a formatting character seems like it would be + a mistake. */ + + /* Would be nice to also be giving the line number. */ + error (0, 0, "loginfo: '%%' not followed by formatting character"); + fmt_begin = fmt_percent + 1; + fmt_end = fmt_begin; + fmt_continue = fmt_begin; + } + else if (*(fmt_percent + 1) == '{') + { + /* The percent has a set of characters following it. */ + + fmt_begin = fmt_percent + 2; + fmt_end = strchr (fmt_begin, '}'); + if (fmt_end) + { + /* Skip over the '}' character. */ + + fmt_continue = fmt_end + 1; + } + else + { + /* There was no close brace -- assume that format + string continues to the end of the line. */ + + /* Would be nice to also be giving the line number. */ + error (0, 0, "loginfo: '}' missing"); + fmt_end = fmt_begin + strlen (fmt_begin); + fmt_continue = fmt_end; + } + } + else + { + /* The percent has a single character following it. FIXME: + %% should expand to a regular percent sign. */ + + fmt_begin = fmt_percent + 1; + fmt_end = fmt_begin + 1; + fmt_continue = fmt_end; + } + + len = fmt_end - fmt_begin; + str_list_format = xmalloc (len + 1); + strncpy (str_list_format, fmt_begin, len); + str_list_format[len] = '\0'; + + /* Allocate an initial chunk of memory. As we build up the string + we will realloc it. */ + if (!str_list) + str_list = xmalloc (1); + str_list[0] = '\0'; + + /* Add entries to the string. Don't bother looking for + entries if the format string is empty. */ + + if (str_list_format[0] != '\0') + { + type = T_TITLE; + (void) walklist (changes, title_proc, NULL); + type = T_ADDED; + (void) walklist (changes, title_proc, NULL); + type = T_MODIFIED; + (void) walklist (changes, title_proc, NULL); + type = T_REMOVED; + (void) walklist (changes, title_proc, NULL); + } + + free (str_list_format); + + /* Construct the final string. */ + + srepos = Short_Repository (repository); + + prog = xmalloc ((fmt_percent - filter) + strlen (srepos) + + strlen (str_list) + strlen (fmt_continue) + + 10); + (void) strncpy (prog, filter, fmt_percent - filter); + prog[fmt_percent - filter] = '\0'; + (void) strcat (prog, "'"); + (void) strcat (prog, srepos); + (void) strcat (prog, str_list); + (void) strcat (prog, "'"); + (void) strcat (prog, fmt_continue); + + /* To be nice, free up some memory. */ + + free (str_list); + str_list = (char *) NULL; + } + else + { + /* There's no format string. */ + prog = xstrdup (filter); + } + + if ((pipefp = run_popen (prog, "w")) == NULL) + { + if (!noexec) + error (0, 0, "cannot write entry to log filter: %s", prog); + free (prog); + return (1); + } + (void) fprintf (pipefp, "Update of %s\n", repository); + (void) fprintf (pipefp, "In directory %s:", hostname); + cp = xgetwd (); + if (cp == NULL) + fprintf (pipefp, "<cannot get working directory: %s>\n\n", + strerror (errno)); + else + { + fprintf (pipefp, "%s\n\n", cp); + free (cp); + } + + setup_tmpfile (pipefp, "", changes); + (void) fprintf (pipefp, "Log Message:\n%s\n", message); + if (logfp != (FILE *) 0) + { + (void) fprintf (pipefp, "Status:\n"); + rewind (logfp); + while ((c = getc (logfp)) != EOF) + (void) putc ((char) c, pipefp); + } + free (prog); + pipestatus = pclose (pipefp); + return ((pipestatus == -1) || (pipestatus == 127)) ? 1 : 0; +} + +/* + * We choose to use the *last* match within the editinfo file for this + * repository. This allows us to have a global editinfo program for the + * root of some hierarchy, for example, and different ones within different + * sub-directories of the root (like a special checker for changes made to + * the "src" directory versus changes made to the "doc" or "test" + * directories. + */ +/* ARGSUSED */ +static int +editinfo_proc(repository, editor) + char *repository; + char *editor; +{ + /* nothing to do if the last match is the same as this one */ + if (editinfo_editor && strcmp (editinfo_editor, editor) == 0) + return (0); + if (editinfo_editor) + free (editinfo_editor); + + editinfo_editor = xstrdup (editor); + return (0); +} + +/* This routine is calld by Parse_Info. it asigns the name of the + * message verification script to the global variable verify_script + */ +static int +verifymsg_proc (repository, script) + char *repository; + char *script; +{ + if (verifymsg_script && strcmp (verifymsg_script, script) == 0) + return (0); + if (verifymsg_script) + free (verifymsg_script); + verifymsg_script = xstrdup (script); + return (0); +} diff --git a/gnu/dist/cvs/src/main.c b/gnu/dist/cvs/src/main.c new file mode 100644 index 00000000000..ba6b1cae4ef --- /dev/null +++ b/gnu/dist/cvs/src/main.c @@ -0,0 +1,1151 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License + * as specified in the README file that comes with the CVS source distribution. + * + * This is the main C driver for the CVS system. + * + * Credit to Dick Grune, Vrije Universiteit, Amsterdam, for writing + * the shell-script CVS system that this is based on. + * + */ + +#include <assert.h> +#include "cvs.h" + +#ifdef HAVE_WINSOCK_H +#include <winsock.h> +#else +extern int gethostname (); +#endif + +char *program_name; +char *program_path; +char *command_name; + +/* I'd dynamically allocate this, but it seems like gethostname + requires a fixed size array. If I'm remembering the RFCs right, + 256 should be enough. */ +#ifndef MAXHOSTNAMELEN +#define MAXHOSTNAMELEN 256 +#endif + +char hostname[MAXHOSTNAMELEN]; + +int use_editor = 1; +int use_cvsrc = 1; +int cvswrite = !CVSREAD_DFLT; +int really_quiet = 0; +int quiet = 0; +int trace = 0; +int noexec = 0; +int logoff = 0; + +/* Set if we should be writing CVSADM directories at top level. At + least for now we'll make the default be off (the CVS 1.9, not CVS + 1.9.2, behavior). */ +int top_level_admin = 0; + +mode_t cvsumask = UMASK_DFLT; + +char *CurDir; + +/* + * Defaults, for the environment variables that are not set + */ +char *Tmpdir = TMPDIR_DFLT; +char *Editor = EDITOR_DFLT; + + +/* When our working directory contains subdirectories with different + values in CVS/Root files, we maintain a list of them. */ +List *root_directories = NULL; + +/* We step through the above values. This variable is set to reflect + the currently active value. */ +char *current_root = NULL; + + +static const struct cmd +{ + char *fullname; /* Full name of the function (e.g. "commit") */ + + /* Synonyms for the command, nick1 and nick2. We supply them + mostly for two reasons: (1) CVS has always supported them, and + we need to maintain compatibility, (2) if there is a need for a + version which is shorter than the fullname, for ease in typing. + Synonyms have the disadvantage that people will see "new" and + then have to think about it, or look it up, to realize that is + the operation they know as "add". Also, this means that one + cannot create a command "cvs new" with a different meaning. So + new synonyms are probably best used sparingly, and where used + should be abbreviations of the fullname (preferably consisting + of the first 2 or 3 or so letters). + + One thing that some systems do is to recognize any unique + abbreviation, for example "annotat" "annota", etc., for + "annotate". The problem with this is that scripts and user + habits will expect a certain abbreviation to be unique, and in + a future release of CVS it may not be. So it is better to + accept only an explicit list of abbreviations and plan on + supporting them in the future as well as now. */ + + char *nick1; + char *nick2; + + int (*func) (); /* Function takes (argc, argv) arguments. */ +} cmds[] = + +{ + { "add", "ad", "new", add }, + { "admin", "adm", "rcs", admin }, + { "annotate", "ann", NULL, annotate }, + { "checkout", "co", "get", checkout }, + { "commit", "ci", "com", commit }, + { "diff", "di", "dif", diff }, + { "edit", NULL, NULL, edit }, + { "editors", NULL, NULL, editors }, + { "export", "exp", "ex", checkout }, + { "history", "hi", "his", history }, + { "import", "im", "imp", import }, + { "init", NULL, NULL, init }, +#ifdef SERVER_SUPPORT + { "kserver", NULL, NULL, server }, /* placeholder */ +#endif + { "log", "lo", "rlog", cvslog }, +#ifdef AUTH_CLIENT_SUPPORT + { "login", "logon", "lgn", login }, + { "logout", NULL, NULL, logout }, +#ifdef SERVER_SUPPORT + { "pserver", NULL, NULL, server }, /* placeholder */ +#endif +#endif /* AUTH_CLIENT_SUPPORT */ + { "rdiff", "patch", "pa", patch }, + { "release", "re", "rel", release }, + { "remove", "rm", "delete", cvsremove }, + { "status", "st", "stat", cvsstatus }, + { "rtag", "rt", "rfreeze", rtag }, + { "tag", "ta", "freeze", cvstag }, + { "unedit", NULL, NULL, unedit }, + { "update", "up", "upd", update }, + { "watch", NULL, NULL, watch }, + { "watchers", NULL, NULL, watchers }, +#ifdef SERVER_SUPPORT + { "server", NULL, NULL, server }, +#endif + { NULL, NULL, NULL, NULL }, +}; + +static const char *const usg[] = +{ + /* CVS usage messages never have followed the GNU convention of + putting metavariables in uppercase. I don't know whether that + is a good convention or not, but if it changes it would have to + change in all the usage messages. For now, they consistently + use lowercase, as far as I know. Puncutation is pretty funky, + though. Sometimes they use none, as here. Sometimes they use + single quotes (not the TeX-ish `' stuff), as in --help-options. + Sometimes they use double quotes, as in cvs -H add. + + Most (not all) of the usage messages seem to have periods at + the end of each line. I haven't tried to duplicate this style + in --help as it is a rather different format from the rest. */ + + "Usage: %s [cvs-options] command [command-options-and-arguments]\n", + " where cvs-options are -q, -n, etc.\n", + " (specify --help-options for a list of options)\n", + " where command is add, admin, etc.\n", + " (specify --help-commands for a list of commands\n", + " or --help-synonyms for a list of command synonyms)\n", + " where command-options-and-arguments depend on the specific command\n", + " (specify -H followed by a command name for command-specific help)\n", + " Specify --help to receive this message\n", + "\n", + + /* Some people think that a bug-reporting address should go here. IMHO, + the web sites are better because anything else is very likely to go + obsolete in the years between a release and when someone might be + reading this help. Besides, we could never adequately discuss + bug reporting in a concise enough way to put in a help message. */ + + /* I was going to put this at the top, but usage() wants the %s to + be in the first line. */ + "The Concurrent Versions System (CVS) is a tool for version control.\n", + /* I really don't think I want to try to define "version control" + in one line. I'm not sure one can get more concise than the + paragraph in ../cvs.spec without assuming the reader knows what + version control means. */ + + "For CVS updates and additional information, see\n", + " Cyclic Software at http://www.cyclic.com/ or\n", + " Pascal Molli's CVS site at http://www.loria.fr/~molli/cvs-index.html\n", + NULL, +}; + +static const char *const cmd_usage[] = +{ + "CVS commands are:\n", + " add Add a new file/directory to the repository\n", + " admin Administration front end for rcs\n", + " annotate Show last revision where each line was modified\n", + " checkout Checkout sources for editing\n", + " commit Check files into the repository\n", + " diff Show differences between revisions\n", + " edit Get ready to edit a watched file\n", + " editors See who is editing a watched file\n", + " export Export sources from CVS, similar to checkout\n", + " history Show repository access history\n", + " import Import sources into CVS, using vendor branches\n", + " init Create a CVS repository if it doesn't exist\n", + " log Print out history information for files\n", +#ifdef AUTH_CLIENT_SUPPORT + " login Prompt for password for authenticating server.\n", + " logout Removes entry in .cvspass for remote repository.\n", +#endif /* AUTH_CLIENT_SUPPORT */ + " rdiff Create 'patch' format diffs between releases\n", + " release Indicate that a Module is no longer in use\n", + " remove Remove an entry from the repository\n", + " rtag Add a symbolic tag to a module\n", + " status Display status information on checked out files\n", + " tag Add a symbolic tag to checked out version of files\n", + " unedit Undo an edit command\n", + " update Bring work tree in sync with repository\n", + " watch Set watches\n", + " watchers See who is watching a file\n", + "(Specify the --help option for a list of other help options)\n", + NULL, +}; + +static const char *const opt_usage[] = +{ + /* Omit -b because it is just for compatibility. */ + "CVS global options (specified before the command name) are:\n", + " -H Displays usage information for command.\n", + " -Q Cause CVS to be really quiet.\n", + " -q Cause CVS to be somewhat quiet.\n", + " -r Make checked-out files read-only.\n", + " -w Make checked-out files read-write (default).\n", + " -l Turn history logging off.\n", + " -n Do not execute anything that will change the disk.\n", + " -t Show trace of program execution -- try with -n.\n", + " -v CVS version and copyright.\n", + " -T tmpdir Use 'tmpdir' for temporary files.\n", + " -e editor Use 'editor' for editing log information.\n", + " -d CVS_root Overrides $CVSROOT as the root of the CVS tree.\n", + " -f Do not use the ~/.cvsrc file.\n", +#ifdef CLIENT_SUPPORT + " -z # Use compression level '#' for net traffic.\n", +#ifdef ENCRYPTION + " -x Encrypt all net traffic.\n", +#endif + " -a Authenticate all net traffic.\n", +#endif + " -s VAR=VAL Set CVS user variable.\n", + "(Specify the --help option for a list of other help options)\n", + NULL +}; + + +static int +set_root_directory (p, ignored) + Node *p; + void *ignored; +{ + if (current_root == NULL && p->data == NULL) + { + current_root = p->key; + return 1; + } + return 0; +} + + +static const char * const* +cmd_synonyms () +{ + char ** synonyms; + char ** line; + const struct cmd *c = &cmds[0]; + /* Three more for title, "specify --help" line, and NULL. */ + int numcmds = 3; + + while (c->fullname != NULL) + { + numcmds++; + c++; + } + + synonyms = (char **) xmalloc(numcmds * sizeof(char *)); + line = synonyms; + *line++ = "CVS command synonyms are:\n"; + for (c = &cmds[0]; c->fullname != NULL; c++) + { + if (c->nick1 || c->nick2) + { + *line = xmalloc (strlen (c->fullname) + + (c->nick1 != NULL ? strlen (c->nick1) : 0) + + (c->nick2 != NULL ? strlen (c->nick2) : 0) + + 40); + sprintf(*line, " %-12s %s %s\n", c->fullname, + c->nick1 ? c->nick1 : "", + c->nick2 ? c->nick2 : ""); + line++; + } + } + *line++ = "(Specify the --help option for a list of other help options)\n"; + *line = NULL; + + return (const char * const*) synonyms; /* will never be freed */ +} + + +unsigned long int +lookup_command_attribute (cmd_name) + char *cmd_name; +{ + unsigned long int ret = 0; + + if (strcmp (cmd_name, "import") != 0) + { + ret |= CVS_CMD_IGNORE_ADMROOT; + } + + + /* The following commands do not use a checked-out working + directory. We conservatively assume that everything else does. + Feel free to add to this list if you are _certain_ something + something doesn't use the WD. */ + if ((strcmp (cmd_name, "checkout") != 0) && + (strcmp (cmd_name, "init") != 0) && + (strcmp (cmd_name, "login") != 0) && + (strcmp (cmd_name, "logout") != 0) && + (strcmp (cmd_name, "rdiff") != 0) && + (strcmp (cmd_name, "release") != 0) && + (strcmp (cmd_name, "rtag") != 0)) + { + ret |= CVS_CMD_USES_WORK_DIR; + } + + + /* The following commands do not modify the repository; we + conservatively assume that everything else does. Feel free to + add to this list if you are _certain_ something is safe. */ + if ((strcmp (cmd_name, "annotate") != 0) && + (strcmp (cmd_name, "checkout") != 0) && + (strcmp (cmd_name, "diff") != 0) && + (strcmp (cmd_name, "rdiff") != 0) && + (strcmp (cmd_name, "update") != 0) && + (strcmp (cmd_name, "editors") != 0) && + (strcmp (cmd_name, "export") != 0) && + (strcmp (cmd_name, "history") != 0) && + (strcmp (cmd_name, "log") != 0) && + (strcmp (cmd_name, "noop") != 0) && + (strcmp (cmd_name, "watchers") != 0) && + (strcmp (cmd_name, "status") != 0)) + { + ret |= CVS_CMD_MODIFIES_REPOSITORY; + } + + return ret; +} + + +static RETSIGTYPE +main_cleanup (sig) + int sig; +{ +#ifndef DONT_USE_SIGNALS + const char *name; + char temp[10]; + + switch (sig) + { +#ifdef SIGHUP + case SIGHUP: + name = "hangup"; + break; +#endif +#ifdef SIGINT + case SIGINT: + name = "interrupt"; + break; +#endif +#ifdef SIGQUIT + case SIGQUIT: + name = "quit"; + break; +#endif +#ifdef SIGPIPE + case SIGPIPE: + name = "broken pipe"; + break; +#endif +#ifdef SIGTERM + case SIGTERM: + name = "termination"; + break; +#endif + default: + /* This case should never be reached, because we list above all + the signals for which we actually establish a signal handler. */ + sprintf (temp, "%d", sig); + name = temp; + break; + } + + error (1, 0, "received %s signal", name); +#endif /* !DONT_USE_SIGNALS */ +} + +int +main (argc, argv) + int argc; + char **argv; +{ + char *CVSroot = CVSROOT_DFLT; + extern char *version_string; + extern char *config_string; + char *cp, *end; + const struct cmd *cm; + int c, err = 0; + int tmpdir_update_env, cvs_update_env; + int free_CVSroot = 0; + int free_Editor = 0; + int free_Tmpdir = 0; + + int help = 0; /* Has the user asked for help? This + lets us support the `cvs -H cmd' + convention to give help for cmd. */ + static struct option long_options[] = + { + {"help", 0, NULL, 'H'}, + {"version", 0, NULL, 'v'}, + {"help-commands", 0, NULL, 1}, + {"help-synonyms", 0, NULL, 2}, + {"help-options", 0, NULL, 4}, + {"allow-root", required_argument, NULL, 3}, + {0, 0, 0, 0} + }; + /* `getopt_long' stores the option index here, but right now we + don't use it. */ + int option_index = 0; + +#ifdef SYSTEM_INITIALIZE + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_INITIALIZE (&argc, &argv); +#endif + +#ifdef HAVE_TZSET + /* On systems that have tzset (which is almost all the ones I know + of), it's a good idea to call it. */ + tzset (); +#endif + + /* + * Just save the last component of the path for error messages + */ + program_path = xstrdup (argv[0]); +#ifdef ARGV0_NOT_PROGRAM_NAME + /* On some systems, e.g. VMS, argv[0] is not the name of the command + which the user types to invoke the program. */ + program_name = "cvs"; +#else + program_name = last_component (argv[0]); +#endif + + /* + * Query the environment variables up-front, so that + * they can be overridden by command line arguments + */ + cvs_update_env = 0; + tmpdir_update_env = *Tmpdir; /* TMPDIR_DFLT must be set */ + if ((cp = getenv (TMPDIR_ENV)) != NULL) + { + Tmpdir = cp; + tmpdir_update_env = 0; /* it's already there */ + } + if ((cp = getenv (EDITOR1_ENV)) != NULL) + Editor = cp; + else if ((cp = getenv (EDITOR2_ENV)) != NULL) + Editor = cp; + else if ((cp = getenv (EDITOR3_ENV)) != NULL) + Editor = cp; + if ((cp = getenv (CVSROOT_ENV)) != NULL) + { + CVSroot = cp; + cvs_update_env = 0; /* it's already there */ + } + if (getenv (CVSREAD_ENV) != NULL) + cvswrite = 0; + + /* Set this to 0 to force getopt initialization. getopt() sets + this to 1 internally. */ + optind = 0; + + /* We have to parse the options twice because else there is no + chance to avoid reading the global options from ".cvsrc". Set + opterr to 0 for avoiding error messages about invalid options. + */ + opterr = 0; + + while ((c = getopt_long + (argc, argv, "+f", NULL, NULL)) + != EOF) + { + if (c == 'f') + use_cvsrc = 0; + } + + /* + * Scan cvsrc file for global options. + */ + if (use_cvsrc) + read_cvsrc (&argc, &argv, "cvs"); + + optind = 0; + opterr = 1; + + while ((c = getopt_long + (argc, argv, "+Qqrwtnlvb:T:e:d:Hfz:s:xa", long_options, &option_index)) + != EOF) + { + switch (c) + { + case 1: + /* --help-commands */ + usage (cmd_usage); + break; + case 2: + /* --help-synonyms */ + usage (cmd_synonyms()); + break; + case 4: + /* --help-options */ + usage (opt_usage); + break; + case 3: + /* --allow-root */ + root_allow_add (optarg); + break; + case 'Q': + really_quiet = 1; + /* FALL THROUGH */ + case 'q': + quiet = 1; + break; + case 'r': + cvswrite = 0; + break; + case 'w': + cvswrite = 1; + break; + case 't': + trace = 1; + break; + case 'n': + noexec = 1; + case 'l': /* Fall through */ + logoff = 1; + break; + case 'v': + /* Having the year here is a good idea, so people have + some idea of how long ago their version of CVS was + released. */ + (void) fputs (version_string, stdout); + (void) fputs (config_string, stdout); + (void) fputs ("\n", stdout); + (void) fputs ("\ +Copyright (c) 1989-1998 Brian Berliner, david d `zoo' zuhn, \n\ + Jeff Polk, and other authors\n", stdout); + (void) fputs ("\n", stdout); + (void) fputs ("CVS may be copied only under the terms of the GNU General Public License,\n", stdout); + (void) fputs ("a copy of which can be found with the CVS distribution kit.\n", stdout); + (void) fputs ("\n", stdout); + + (void) fputs ("Specify the --help option for further information about CVS\n", stdout); + + exit (0); + break; + case 'b': + /* This option used to specify the directory for RCS + executables. But since we don't run them any more, + this is a noop. Silently ignore it so that .cvsrc + and scripts and inetd.conf and such can work with + either new or old CVS. */ + break; + case 'T': + Tmpdir = xstrdup (optarg); + free_Tmpdir = 1; + tmpdir_update_env = 1; /* need to update environment */ + break; + case 'e': + Editor = xstrdup (optarg); + free_Editor = 1; + break; + case 'd': + if (CVSroot_cmdline != NULL) + free (CVSroot_cmdline); + CVSroot_cmdline = xstrdup (optarg); + CVSroot = xstrdup (optarg); + free_CVSroot = 1; + cvs_update_env = 1; /* need to update environment */ + break; + case 'H': + help = 1; + break; + case 'f': + use_cvsrc = 0; /* unnecessary, since we've done it above */ + break; + case 'z': +#ifdef CLIENT_SUPPORT + gzip_level = atoi (optarg); + if (gzip_level <= 0 || gzip_level > 9) + error (1, 0, + "gzip compression level must be between 1 and 9"); +#endif + /* If no CLIENT_SUPPORT, we just silently ignore the gzip + level, so that users can have it in their .cvsrc and not + cause any trouble. */ + break; + case 's': + variable_set (optarg); + break; + case 'x': +#ifdef CLIENT_SUPPORT + cvsencrypt = 1; +#endif /* CLIENT_SUPPORT */ + /* If no CLIENT_SUPPORT, ignore -x, so that users can + have it in their .cvsrc and not cause any trouble. + If no ENCRYPTION, we still accept -x, but issue an + error if we are being run as a client. */ + break; + case 'a': +#ifdef CLIENT_SUPPORT + cvsauthenticate = 1; +#endif + /* If no CLIENT_SUPPORT, ignore -a, so that users can + have it in their .cvsrc and not cause any trouble. + We will issue an error later if stream + authentication is not supported. */ + break; + case '?': + default: + usage (usg); + } + } + + argc -= optind; + argv += optind; + if (argc < 1) + usage (usg); + + + /* Look up the command name. */ + + command_name = argv[0]; + for (cm = cmds; cm->fullname; cm++) + { + if (cm->nick1 && !strcmp (command_name, cm->nick1)) + break; + if (cm->nick2 && !strcmp (command_name, cm->nick2)) + break; + if (!strcmp (command_name, cm->fullname)) + break; + } + + if (!cm->fullname) + usage (cmd_usage); /* no match */ + else + command_name = cm->fullname; /* Global pointer for later use */ + + /* This should probably remain a warning, rather than an error, + for quite a while. For one thing the version of VC distributed + with GNU emacs 19.34 invokes 'cvs rlog' instead of 'cvs log'. */ + if (strcmp (argv[0], "rlog") == 0) + { + error (0, 0, "warning: the rlog command is deprecated"); + error (0, 0, "use the synonymous log command instead"); + } + + if (help) + { + argc = -1; /* some functions only check for this */ + err = (*(cm->func)) (argc, argv); + } + else + { + /* The user didn't ask for help, so go ahead and authenticate, + set up CVSROOT, and the rest of it. */ + + /* The UMASK environment variable isn't handled with the + others above, since we don't want to signal errors if the + user has asked for help. This won't work if somebody adds + a command-line flag to set the umask, since we'll have to + parse it before we get here. */ + + if ((cp = getenv (CVSUMASK_ENV)) != NULL) + { + /* FIXME: Should be accepting symbolic as well as numeric mask. */ + cvsumask = strtol (cp, &end, 8) & 0777; + if (*end != '\0') + error (1, errno, "invalid umask value in %s (%s)", + CVSUMASK_ENV, cp); + } + +#if defined (HAVE_KERBEROS) && defined (SERVER_SUPPORT) + /* If we are invoked with a single argument "kserver", then we are + running as Kerberos server as root. Do the authentication as + the very first thing, to minimize the amount of time we are + running as root. */ + if (strcmp (command_name, "kserver") == 0) + { + kserver_authenticate_connection (); + + /* Pretend we were invoked as a plain server. */ + command_name = "server"; + } +#endif /* HAVE_KERBEROS */ + + +#if (defined(AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI)) && defined(SERVER_SUPPORT) + if (strcmp (command_name, "pserver") == 0) + { + /* The reason that --allow-root is not a command option + is mainly the comment in server() about how argc,argv + might be from .cvsrc. I'm not sure about that, and + I'm not sure it is only true of command options, but + it seems easier to make it a global option. */ + + /* Gets username and password from client, authenticates, then + switches to run as that user and sends an ACK back to the + client. */ + pserver_authenticate_connection (); + + /* Pretend we were invoked as a plain server. */ + command_name = "server"; + } +#endif /* (AUTH_SERVER_SUPPORT || HAVE_GSSAPI) && SERVER_SUPPORT */ + +#ifdef SERVER_SUPPORT + server_active = strcmp (command_name, "server") == 0; +#endif + + /* This is only used for writing into the history file. For + remote connections, it might be nice to have hostname + and/or remote path, on the other hand I'm not sure whether + it is worth the trouble. */ + +#ifdef SERVER_SUPPORT + if (server_active) + CurDir = xstrdup ("<remote>"); + else +#endif + { + CurDir = xgetwd (); + if (CurDir == NULL) + error (1, errno, "cannot get working directory"); + } + + if (Tmpdir == NULL || Tmpdir[0] == '\0') + Tmpdir = "/tmp"; + +#ifdef HAVE_PUTENV + if (tmpdir_update_env) + { + char *env; + env = xmalloc (strlen (TMPDIR_ENV) + strlen (Tmpdir) + 1 + 1); + (void) sprintf (env, "%s=%s", TMPDIR_ENV, Tmpdir); + (void) putenv (env); + /* do not free env, as putenv has control of it */ + } +#endif + +#ifndef DONT_USE_SIGNALS + /* make sure we clean up on error */ +#ifdef SIGHUP + (void) SIG_register (SIGHUP, main_cleanup); + (void) SIG_register (SIGHUP, Lock_Cleanup); +#endif +#ifdef SIGINT + (void) SIG_register (SIGINT, main_cleanup); + (void) SIG_register (SIGINT, Lock_Cleanup); +#endif +#ifdef SIGQUIT + (void) SIG_register (SIGQUIT, main_cleanup); + (void) SIG_register (SIGQUIT, Lock_Cleanup); +#endif +#ifdef SIGPIPE + (void) SIG_register (SIGPIPE, main_cleanup); + (void) SIG_register (SIGPIPE, Lock_Cleanup); +#endif +#ifdef SIGTERM + (void) SIG_register (SIGTERM, main_cleanup); + (void) SIG_register (SIGTERM, Lock_Cleanup); +#endif +#endif /* !DONT_USE_SIGNALS */ + + gethostname(hostname, sizeof (hostname)); + +#ifdef KLUDGE_FOR_WNT_TESTSUITE + /* Probably the need for this will go away at some point once + we call fflush enough places (e.g. fflush (stdout) in + cvs_outerr). */ + (void) setvbuf (stdout, (char *) NULL, _IONBF, 0); + (void) setvbuf (stderr, (char *) NULL, _IONBF, 0); +#endif /* KLUDGE_FOR_WNT_TESTSUITE */ + + if (use_cvsrc) + read_cvsrc (&argc, &argv, command_name); + +#ifdef SERVER_SUPPORT + /* Fiddling with CVSROOT doesn't make sense if we're running + in server mode, since the client will send the repository + directory after the connection is made. */ + + if (!server_active) +#endif + { + char *CVSADM_Root; + + /* See if we are able to find a 'better' value for CVSroot + in the CVSADM_ROOT directory. */ + + CVSADM_Root = NULL; + + /* "cvs import" shouldn't check CVS/Root; in general it + ignores CVS directories and CVS/Root is likely to + specify a different repository than the one we are + importing to. */ + + if ((lookup_command_attribute (command_name) + & CVS_CMD_IGNORE_ADMROOT) + + /* -d overrides CVS/Root, so don't give an error if the + latter points to a nonexistent repository. */ + && CVSroot_cmdline == NULL) + { + CVSADM_Root = Name_Root((char *) NULL, (char *) NULL); + } + + if (CVSADM_Root != NULL) + { + if (CVSroot == NULL || !cvs_update_env) + { + CVSroot = CVSADM_Root; + cvs_update_env = 1; /* need to update environment */ + } + } + + /* Now we've reconciled CVSROOT from the command line, the + CVS/Root file, and the environment variable. Do the + last sanity checks on the variable. */ + + if (! CVSroot) + { + error (0, 0, + "No CVSROOT specified! Please use the `-d' option"); + error (1, 0, + "or set the %s environment variable.", CVSROOT_ENV); + } + + if (! *CVSroot) + { + error (0, 0, + "CVSROOT is set but empty! Make sure that the"); + error (0, 0, + "specification of CVSROOT is legal, either via the"); + error (0, 0, + "`-d' option, the %s environment variable, or the", + CVSROOT_ENV); + error (1, 0, + "CVS/Root file (if any)."); + } + } + + /* Here begins the big loop over unique cvsroot values. We + need to call do_recursion once for each unique value found + in CVS/Root. Prime the list with the current value. */ + + /* Create the list. */ + assert (root_directories == NULL); + root_directories = getlist (); + + /* Prime it. */ + if (CVSroot != NULL) + { + Node *n; + n = getnode (); + n->type = NT_UNKNOWN; + n->key = xstrdup (CVSroot); + n->data = NULL; + + if (addnode (root_directories, n)) + error (1, 0, "cannot add initial CVSROOT %s", n->key); + } + + assert (current_root == NULL); + + /* If we're running the server, we want to execute this main + loop once and only once (we won't be serving multiple roots + from this connection, so there's no need to do it more than + once). To get out of the loop, we perform a "break" at the + end of things. */ + + while ( +#ifdef SERVER_SUPPORT + server_active || +#endif + walklist (root_directories, set_root_directory, NULL) + ) + { +#ifdef SERVER_SUPPORT + /* Fiddling with CVSROOT doesn't make sense if we're running + in server mode, since the client will send the repository + directory after the connection is made. */ + + if (!server_active) +#endif + { + /* Now we're 100% sure that we have a valid CVSROOT + variable. Parse it to see if we're supposed to do + remote accesses or use a special access method. */ + + if (parse_cvsroot (current_root)) + error (1, 0, "Bad CVSROOT."); + + if (trace) + error (0, 0, "notice: main loop with CVSROOT=%s", + current_root); + + /* + * Check to see if we can write into the history file. If not, + * we assume that we can't work in the repository. + * BUT, only if the history file exists. + */ + + if (!client_active) + { + char *path; + int save_errno; + + path = xmalloc (strlen (CVSroot_directory) + + sizeof (CVSROOTADM) + + 20 + + sizeof (CVSROOTADM_HISTORY)); + (void) sprintf (path, "%s/%s", CVSroot_directory, CVSROOTADM); + if (!isaccessible (path, R_OK | X_OK)) + { + save_errno = errno; + /* If this is "cvs init", the root need not exist yet. */ + if (strcmp (command_name, "init") != 0) + { + error (1, save_errno, "%s", path); + } + } + (void) strcat (path, "/"); + (void) strcat (path, CVSROOTADM_HISTORY); + if (isfile (path) && !isaccessible (path, R_OK | W_OK)) + { + save_errno = errno; + error (0, 0, "Sorry, you don't have read/write access to the history file"); + error (1, save_errno, "%s", path); + } + free (path); + } + +#ifdef HAVE_PUTENV + /* Update the CVSROOT environment variable if necessary. */ + /* FIXME (njc): should we always set this with the CVSROOT from the command line? */ + if (cvs_update_env) + { + char *env; + env = xmalloc (strlen (CVSROOT_ENV) + strlen (CVSroot) + + 1 + 1); + (void) sprintf (env, "%s=%s", CVSROOT_ENV, CVSroot); + (void) putenv (env); + /* do not free env, as putenv has control of it */ + } +#endif + } + + /* Parse the CVSROOT/config file, but only for local. For the + server, we parse it after we know $CVSROOT. For the + client, it doesn't get parsed at all, obviously. The + presence of the parse_config call here is not mean to + predetermine whether CVSROOT/config overrides things from + read_cvsrc and other such places or vice versa. That sort + of thing probably needs more thought. */ + if (1 +#ifdef SERVER_SUPPORT + && !server_active +#endif +#ifdef CLIENT_SUPPORT + && !client_active +#endif + ) + { + /* If there was an error parsing the config file, parse_config + already printed an error. We keep going. Why? Because + if we didn't, then there would be no way to check in a new + CVSROOT/config file to fix the broken one! */ + parse_config (CVSroot_directory); + } + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* Create a new list for directory names that we've + sent to the server. */ + if (dirs_sent_to_server != NULL) + dellist (&dirs_sent_to_server); + dirs_sent_to_server = getlist (); + } +#endif + + err = (*(cm->func)) (argc, argv); + + /* Mark this root directory as done. When the server is + active, current_root will be NULL -- don't try and + remove it from the list. */ + + if (current_root != NULL) + { + Node *n = findnode (root_directories, current_root); + assert (n != NULL); + n->data = (void *) 1; + current_root = NULL; + } + +#if 0 + /* This will not work yet, since it tries to free (void *) 1. */ + dellist (&root_directories); +#endif + +#ifdef SERVER_SUPPORT + if (server_active) + break; +#endif + } /* end of loop for cvsroot values */ + + } /* end of stuff that gets done if the user DOESN'T ask for help */ + + Lock_Cleanup (); + + free (program_path); + if (CVSroot_cmdline != NULL) + free (CVSroot_cmdline); + if (free_CVSroot) + free (CVSroot); + if (free_Editor) + free (Editor); + if (free_Tmpdir) + free (Tmpdir); + root_allow_free (); + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + /* This is exit rather than return because apparently that keeps + some tools which check for memory leaks happier. */ + exit (err ? EXIT_FAILURE : 0); + /* Keep picky/stupid compilers (e.g. Visual C++ 5.0) happy. */ + return 0; +} + +char * +Make_Date (rawdate) + char *rawdate; +{ + time_t unixtime; + + unixtime = get_date (rawdate, (struct timeb *) NULL); + if (unixtime == (time_t) - 1) + error (1, 0, "Can't parse date/time: %s", rawdate); + return date_from_time_t (unixtime); +} + +/* Convert a time_t to an RCS format date. This is mainly for the + use of "cvs history", because the CVSROOT/history file contains + time_t format dates; most parts of CVS will want to avoid using + time_t's directly, and instead use RCS_datecmp, Make_Date, &c. + Assuming that the time_t is in GMT (as it generally should be), + then the result will be in GMT too. + + Returns a newly malloc'd string. */ + +char * +date_from_time_t (unixtime) + time_t unixtime; +{ + struct tm *ftm; + char date[MAXDATELEN]; + char *ret; + + ftm = gmtime (&unixtime); + if (ftm == NULL) + /* This is a system, like VMS, where the system clock is in local + time. Hopefully using localtime here matches the "zero timezone" + hack I added to get_date (get_date of course being the relevant + issue for Make_Date, and for history.c too I think). */ + ftm = localtime (&unixtime); + + (void) sprintf (date, DATEFORM, + ftm->tm_year + (ftm->tm_year < 100 ? 0 : 1900), + ftm->tm_mon + 1, ftm->tm_mday, ftm->tm_hour, + ftm->tm_min, ftm->tm_sec); + ret = xstrdup (date); + return (ret); +} + +/* Convert a date to RFC822/1123 format. This is used in contexts like + dates to send in the protocol; it should not vary based on locale or + other such conventions for users. We should have another routine which + does that kind of thing. + + The SOURCE date is in our internal RCS format. DEST should point to + storage managed by the caller, at least MAXDATELEN characters. */ +void +date_to_internet (dest, source) + char *dest; + char *source; +{ + int year, month, day, hour, minute, second; + + /* Just to reiterate, these strings are from RFC822 and do not vary + according to locale. */ + static const char *const month_names[] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + + if (sscanf (source, SDATEFORM, + &year, &month, &day, &hour, &minute, &second) + != 6) + /* Is there a better way to handle errors here? I made this + non-fatal in case we are called from the code which can't + deal with fatal errors. */ + error (0, 0, "internal error: bad date %s", source); + + /* Always send a four digit year. */ + if (year < 100) + year += 1900; + + sprintf (dest, "%d %s %d %02d:%02d:%02d -0000", day, + month < 1 || month > 12 ? "???" : month_names[month - 1], + year, hour, minute, second); +} + +void +usage (cpp) + register const char *const *cpp; +{ + (void) fprintf (stderr, *cpp++, program_name, command_name); + for (; *cpp; cpp++) + (void) fprintf (stderr, *cpp); + error_exit (); +} diff --git a/gnu/dist/cvs/src/mkmodules.c b/gnu/dist/cvs/src/mkmodules.c new file mode 100644 index 00000000000..96661845373 --- /dev/null +++ b/gnu/dist/cvs/src/mkmodules.c @@ -0,0 +1,946 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS kit. */ + +#include "cvs.h" +#include "savecwd.h" +#include "getline.h" + +#ifndef DBLKSIZ +#define DBLKSIZ 4096 /* since GNU ndbm doesn't define it */ +#endif + +static int checkout_file PROTO((char *file, char *temp)); +static char *make_tempfile PROTO((void)); +static void rename_rcsfile PROTO((char *temp, char *real)); + +#ifndef MY_NDBM +static void rename_dbmfile PROTO((char *temp)); +static void write_dbmfile PROTO((char *temp)); +#endif /* !MY_NDBM */ + +/* Structure which describes an administrative file. */ +struct admin_file { + /* Name of the file, within the CVSROOT directory. */ + char *filename; + + /* This is a one line description of what the file is for. It is not + currently used, although one wonders whether it should be, somehow. + If NULL, then don't process this file in mkmodules (FIXME?: a bit of + a kludge; probably should replace this with a flags field). */ + char *errormsg; + + /* Contents which the file should have in a new repository. To avoid + problems with brain-dead compilers which choke on long string constants, + this is a pointer to an array of char * terminated by NULL--each of + the strings is concatenated. + + If this field is NULL, the file is not created in a new + repository, but it can be added with "cvs add" (just as if one + had created the repository with a version of CVS which didn't + know about the file) and the checked-out copy will be updated + without having to add it to checkoutlist. */ + const char * const *contents; +}; + +static const char *const loginfo_contents[] = { + "# The \"loginfo\" file controls where \"cvs commit\" log information\n", + "# is sent. The first entry on a line is a regular expression which must match\n", + "# the directory that the change is being made to, relative to the\n", + "# $CVSROOT. If a match is found, then the remainder of the line is a filter\n", + "# program that should expect log information on its standard input.\n", + "#\n", + "# If the repository name does not match any of the regular expressions in this\n", + "# file, the \"DEFAULT\" line is used, if it is specified.\n", + "#\n", + "# If the name ALL appears as a regular expression it is always used\n", + "# in addition to the first matching regex or DEFAULT.\n", + "#\n", + "# You may specify a format string as part of the\n", + "# filter. The string is composed of a `%' followed\n", + "# by a single format character, or followed by a set of format\n", + "# characters surrounded by `{' and `}' as separators. The format\n", + "# characters are:\n", + "#\n", + "# s = file name\n", + "# V = old version number (pre-checkin)\n", + "# v = new version number (post-checkin)\n", + "#\n", + "# For example:\n", + "#DEFAULT (echo \"\"; id; echo %s; date; cat) >> $CVSROOT/CVSROOT/commitlog\n", + "# or\n", + "#DEFAULT (echo \"\"; id; echo %{sVv}; date; cat) >> $CVSROOT/CVSROOT/commitlog\n", + NULL +}; + +static const char *const rcsinfo_contents[] = { + "# The \"rcsinfo\" file is used to control templates with which the editor\n", + "# is invoked on commit and import.\n", + "#\n", + "# The first entry on a line is a regular expression which is tested\n", + "# against the directory that the change is being made to, relative to the\n", + "# $CVSROOT. For the first match that is found, then the remainder of the\n", + "# line is the name of the file that contains the template.\n", + "#\n", + "# If the repository name does not match any of the regular expressions in this\n", + "# file, the \"DEFAULT\" line is used, if it is specified.\n", + "#\n", + "# If the name \"ALL\" appears as a regular expression it is always used\n", + "# in addition to the first matching regex or \"DEFAULT\".\n", + NULL +}; + +static const char *const editinfo_contents[] = { + "# The \"editinfo\" file is used to allow verification of logging\n", + "# information. It works best when a template (as specified in the\n", + "# rcsinfo file) is provided for the logging procedure. Given a\n", + "# template with locations for, a bug-id number, a list of people who\n", + "# reviewed the code before it can be checked in, and an external\n", + "# process to catalog the differences that were code reviewed, the\n", + "# following test can be applied to the code:\n", + "#\n", + "# Making sure that the entered bug-id number is correct.\n", + "# Validating that the code that was reviewed is indeed the code being\n", + "# checked in (using the bug-id number or a seperate review\n", + "# number to identify this particular code set.).\n", + "#\n", + "# If any of the above test failed, then the commit would be aborted.\n", + "#\n", + "# Actions such as mailing a copy of the report to each reviewer are\n", + "# better handled by an entry in the loginfo file.\n", + "#\n", + "# One thing that should be noted is the the ALL keyword is not\n", + "# supported. There can be only one entry that matches a given\n", + "# repository.\n", + NULL +}; + +static const char *const verifymsg_contents[] = { + "# The \"verifymsg\" file is used to allow verification of logging\n", + "# information. It works best when a template (as specified in the\n", + "# rcsinfo file) is provided for the logging procedure. Given a\n", + "# template with locations for, a bug-id number, a list of people who\n", + "# reviewed the code before it can be checked in, and an external\n", + "# process to catalog the differences that were code reviewed, the\n", + "# following test can be applied to the code:\n", + "#\n", + "# Making sure that the entered bug-id number is correct.\n", + "# Validating that the code that was reviewed is indeed the code being\n", + "# checked in (using the bug-id number or a seperate review\n", + "# number to identify this particular code set.).\n", + "#\n", + "# If any of the above test failed, then the commit would be aborted.\n", + "#\n", + "# Actions such as mailing a copy of the report to each reviewer are\n", + "# better handled by an entry in the loginfo file.\n", + "#\n", + "# One thing that should be noted is the the ALL keyword is not\n", + "# supported. There can be only one entry that matches a given\n", + "# repository.\n", + NULL +}; + +static const char *const commitinfo_contents[] = { + "# The \"commitinfo\" file is used to control pre-commit checks.\n", + "# The filter on the right is invoked with the repository and a list \n", + "# of files to check. A non-zero exit of the filter program will \n", + "# cause the commit to be aborted.\n", + "#\n", + "# The first entry on a line is a regular expression which is tested\n", + "# against the directory that the change is being committed to, relative\n", + "# to the $CVSROOT. For the first match that is found, then the remainder\n", + "# of the line is the name of the filter to run.\n", + "#\n", + "# If the repository name does not match any of the regular expressions in this\n", + "# file, the \"DEFAULT\" line is used, if it is specified.\n", + "#\n", + "# If the name \"ALL\" appears as a regular expression it is always used\n", + "# in addition to the first matching regex or \"DEFAULT\".\n", + NULL +}; + +static const char *const taginfo_contents[] = { + "# The \"taginfo\" file is used to control pre-tag checks.\n", + "# The filter on the right is invoked with the following arguments:\n", + "#\n", + "# $1 -- tagname\n", + "# $2 -- operation \"add\" for tag, \"mov\" for tag -F, and \"del\" for tag -d\n", + "# $3 -- repository\n", + "# $4-> file revision [file revision ...]\n", + "#\n", + "# A non-zero exit of the filter program will cause the tag to be aborted.\n", + "#\n", + "# The first entry on a line is a regular expression which is tested\n", + "# against the directory that the change is being committed to, relative\n", + "# to the $CVSROOT. For the first match that is found, then the remainder\n", + "# of the line is the name of the filter to run.\n", + "#\n", + "# If the repository name does not match any of the regular expressions in this\n", + "# file, the \"DEFAULT\" line is used, if it is specified.\n", + "#\n", + "# If the name \"ALL\" appears as a regular expression it is always used\n", + "# in addition to the first matching regex or \"DEFAULT\".\n", + NULL +}; + +static const char *const checkoutlist_contents[] = { + "# The \"checkoutlist\" file is used to support additional version controlled\n", + "# administrative files in $CVSROOT/CVSROOT, such as template files.\n", + "#\n", + "# The first entry on a line is a filename which will be checked out from\n", + "# the corresponding RCS file in the $CVSROOT/CVSROOT directory.\n", + "# The remainder of the line is an error message to use if the file cannot\n", + "# be checked out.\n", + "#\n", + "# File format:\n", + "#\n", + "# [<whitespace>]<filename><whitespace><error message><end-of-line>\n", + "#\n", + "# comment lines begin with '#'\n", + NULL +}; + +static const char *const cvswrappers_contents[] = { + "# This file affects handling of files based on their names.\n", + "#\n", + "# The -t/-f options allow one to treat directories of files\n", + "# as a single file, or to transform a file in other ways on\n", + "# its way in and out of CVS.\n", + "#\n", + "# The -m option specifies whether CVS attempts to merge files.\n", + "#\n", + "# The -k option specifies keyword expansion (e.g. -kb for binary).\n", + "#\n", + "# Format of wrapper file ($CVSROOT/CVSROOT/cvswrappers or .cvswrappers)\n", + "#\n", + "# wildcard [option value][option value]...\n", + "#\n", + "# where option is one of\n", + "# -f from cvs filter value: path to filter\n", + "# -t to cvs filter value: path to filter\n", + "# -m update methodology value: MERGE or COPY\n", + "# -k expansion mode value: b, o, kkv, &c\n", + "#\n", + "# and value is a single-quote delimited value.\n", + "# For example:\n", + "#*.gif -k 'b'\n", + NULL +}; + +static const char *const notify_contents[] = { + "# The \"notify\" file controls where notifications from watches set by\n", + "# \"cvs watch add\" or \"cvs edit\" are sent. The first entry on a line is\n", + "# a regular expression which is tested against the directory that the\n", + "# change is being made to, relative to the $CVSROOT. If it matches,\n", + "# then the remainder of the line is a filter program that should contain\n", + "# one occurrence of %s for the user to notify, and information on its\n", + "# standard input.\n", + "#\n", + "# \"ALL\" or \"DEFAULT\" can be used in place of the regular expression.\n", + "#\n", + "# For example:\n", + "#ALL mail %s -s \"CVS notification\"\n", + NULL +}; + +static const char *const modules_contents[] = { + "# Three different line formats are valid:\n", + "# key -a aliases...\n", + "# key [options] directory\n", + "# key [options] directory files...\n", + "#\n", + "# Where \"options\" are composed of:\n", + "# -i prog Run \"prog\" on \"cvs commit\" from top-level of module.\n", + "# -o prog Run \"prog\" on \"cvs checkout\" of module.\n", + "# -e prog Run \"prog\" on \"cvs export\" of module.\n", + "# -t prog Run \"prog\" on \"cvs rtag\" of module.\n", + "# -u prog Run \"prog\" on \"cvs update\" of module.\n", + "# -d dir Place module in directory \"dir\" instead of module name.\n", + "# -l Top-level directory only -- do not recurse.\n", + "#\n", + "# NOTE: If you change any of the \"Run\" options above, you'll have to\n", + "# release and re-checkout any working directories of these modules.\n", + "#\n", + "# And \"directory\" is a path to a directory relative to $CVSROOT.\n", + "#\n", + "# The \"-a\" option specifies an alias. An alias is interpreted as if\n", + "# everything on the right of the \"-a\" had been typed on the command line.\n", + "#\n", + "# You can encode a module within a module by using the special '&'\n", + "# character to interpose another module into the current module. This\n", + "# can be useful for creating a module that consists of many directories\n", + "# spread out over the entire source repository.\n", + NULL +}; + +static const char *const config_contents[] = { + "# Set this to \"no\" if pserver shouldn't check system users/passwords\n", + "#SystemAuth=no\n", + "\n", + "# Set `PreservePermissions' to `yes' to save file status information\n", + "# in the repository.\n", + "#PreservePermissions=no\n", + "\n", + "# Set `TopLevelAdmin' to `yes' to create a CVS directory at the top\n", + "# level of the new working directory when using the `cvs checkout'\n", + "# command.\n", + "#TopLevelAdmin=no\n", + NULL +}; + +static const struct admin_file filelist[] = { + {CVSROOTADM_LOGINFO, + "no logging of 'cvs commit' messages is done without a %s file", + &loginfo_contents[0]}, + {CVSROOTADM_RCSINFO, + "a %s file can be used to configure 'cvs commit' templates", + rcsinfo_contents}, + {CVSROOTADM_EDITINFO, + "a %s file can be used to validate log messages", + editinfo_contents}, + {CVSROOTADM_VERIFYMSG, + "a %s file can be used to validate log messages", + verifymsg_contents}, + {CVSROOTADM_COMMITINFO, + "a %s file can be used to configure 'cvs commit' checking", + commitinfo_contents}, + {CVSROOTADM_TAGINFO, + "a %s file can be used to configure 'cvs tag' checking", + taginfo_contents}, + {CVSROOTADM_IGNORE, + "a %s file can be used to specify files to ignore", + NULL}, + {CVSROOTADM_CHECKOUTLIST, + "a %s file can specify extra CVSROOT files to auto-checkout", + checkoutlist_contents}, + {CVSROOTADM_WRAPPER, + "a %s file can be used to specify files to treat as wrappers", + cvswrappers_contents}, + {CVSROOTADM_NOTIFY, + "a %s file can be used to specify where notifications go", + notify_contents}, + {CVSROOTADM_MODULES, + /* modules is special-cased in mkmodules. */ + NULL, + modules_contents}, + {CVSROOTADM_READERS, + "a %s file specifies read-only users", + NULL}, + {CVSROOTADM_WRITERS, + "a %s file specifies read/write users", + NULL}, + + /* Some have suggested listing CVSROOTADM_PASSWD here too. This + would mean that CVS commands which operate on the + CVSROOTADM_PASSWD file would transmit hashed passwords over the + net. This might seem to be no big deal, as pserver normally + transmits cleartext passwords, but the difference is that + CVSROOTADM_PASSWD contains *all* passwords, not just the ones + currently being used. For example, it could be too easy to + accidentally give someone readonly access to CVSROOTADM_PASSWD + (e.g. via anonymous CVS or cvsweb), and then if there are any + guessable passwords for read/write access (usually there will be) + they get read/write access. + + Another worry is the implications of storing old passwords--if + someone used a password in the past they might be using it + elsewhere, using a similar password, etc, and so saving old + passwords, even hashed, is probably not a good idea. */ + + {CVSROOTADM_CONFIG, + "a %s file configures various behaviors", + config_contents}, + {NULL, NULL, NULL} +}; + +/* Rebuild the checked out administrative files in directory DIR. */ +int +mkmodules (dir) + char *dir; +{ + struct saved_cwd cwd; + char *temp; + char *cp, *last, *fname; +#ifdef MY_NDBM + DBM *db; +#endif + FILE *fp; + char *line = NULL; + size_t line_allocated = 0; + const struct admin_file *fileptr; + + if (save_cwd (&cwd)) + error_exit (); + + if ( CVS_CHDIR (dir) < 0) + error (1, errno, "cannot chdir to %s", dir); + + /* + * First, do the work necessary to update the "modules" database. + */ + temp = make_tempfile (); + switch (checkout_file (CVSROOTADM_MODULES, temp)) + { + + case 0: /* everything ok */ +#ifdef MY_NDBM + /* open it, to generate any duplicate errors */ + if ((db = dbm_open (temp, O_RDONLY, 0666)) != NULL) + dbm_close (db); +#else + write_dbmfile (temp); + rename_dbmfile (temp); +#endif + rename_rcsfile (temp, CVSROOTADM_MODULES); + break; + + default: + error (0, 0, + "'cvs checkout' is less functional without a %s file", + CVSROOTADM_MODULES); + break; + } /* switch on checkout_file() */ + + if (unlink_file (temp) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", temp); + free (temp); + + /* Checkout the files that need it in CVSROOT dir */ + for (fileptr = filelist; fileptr && fileptr->filename; fileptr++) { + if (fileptr->errormsg == NULL) + continue; + temp = make_tempfile (); + if (checkout_file (fileptr->filename, temp) == 0) + rename_rcsfile (temp, fileptr->filename); +#if 0 + /* + * If there was some problem other than the file not existing, + * checkout_file already printed a real error message. If the + * file does not exist, it is harmless--it probably just means + * that the repository was created with an old version of CVS + * which didn't have so many files in CVSROOT. + */ + else if (fileptr->errormsg) + error (0, 0, fileptr->errormsg, fileptr->filename); +#endif + if (unlink_file (temp) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", temp); + free (temp); + } + + fp = CVS_FOPEN (CVSROOTADM_CHECKOUTLIST, "r"); + if (fp) + { + /* + * File format: + * [<whitespace>]<filename><whitespace><error message><end-of-line> + * + * comment lines begin with '#' + */ + while (getline (&line, &line_allocated, fp) >= 0) + { + /* skip lines starting with # */ + if (line[0] == '#') + continue; + + if ((last = strrchr (line, '\n')) != NULL) + *last = '\0'; /* strip the newline */ + + /* Skip leading white space. */ + for (fname = line; + *fname && isspace ((unsigned char) *fname); + fname++) + ; + + /* Find end of filename. */ + for (cp = fname; *cp && !isspace ((unsigned char) *cp); cp++) + ; + *cp = '\0'; + + temp = make_tempfile (); + if (checkout_file (fname, temp) == 0) + { + rename_rcsfile (temp, fname); + } + else + { + for (cp++; + cp < last && *last && isspace ((unsigned char) *last); + cp++) + ; + if (cp < last && *cp) + error (0, 0, cp, fname); + } + if (unlink_file (temp) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", temp); + free (temp); + } + if (line) + free (line); + if (ferror (fp)) + error (0, errno, "cannot read %s", CVSROOTADM_CHECKOUTLIST); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", CVSROOTADM_CHECKOUTLIST); + } + else + { + /* Error from CVS_FOPEN. */ + if (!existence_error (errno)) + error (0, errno, "cannot open %s", CVSROOTADM_CHECKOUTLIST); + } + + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + + return (0); +} + +/* + * Yeah, I know, there are NFS race conditions here. + */ +static char * +make_tempfile () +{ + static int seed = 0; + int fd; + char *temp; + + if (seed == 0) + seed = getpid (); + temp = xmalloc (sizeof (BAKPREFIX) + 40); + while (1) + { + (void) sprintf (temp, "%s%d", BAKPREFIX, seed++); + if ((fd = CVS_OPEN (temp, O_CREAT|O_EXCL|O_RDWR, 0666)) != -1) + break; + if (errno != EEXIST) + error (1, errno, "cannot create temporary file %s", temp); + } + if (close(fd) < 0) + error(1, errno, "cannot close temporary file %s", temp); + return temp; +} + +/* Get a file. If the file does not exist, return 1 silently. If + there is an error, print a message and return 1 (FIXME: probably + not a very clean convention). On success, return 0. */ + +static int +checkout_file (file, temp) + char *file; + char *temp; +{ + char *rcs; + RCSNode *rcsnode; + int retcode = 0; + + if (noexec) + return 0; + + rcs = xmalloc (strlen (file) + 5); + strcpy (rcs, file); + strcat (rcs, RCSEXT); + if (!isfile (rcs)) + { + free (rcs); + return (1); + } + rcsnode = RCS_parsercsfile (rcs); + retcode = RCS_checkout (rcsnode, NULL, NULL, NULL, NULL, temp, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + if (retcode != 0) + { + /* Probably not necessary (?); RCS_checkout already printed a + message. */ + error (0, 0, "failed to check out %s file", + file); + } + freercsnode (&rcsnode); + free (rcs); + return (retcode); +} + +#ifndef MY_NDBM + +static void +write_dbmfile (temp) + char *temp; +{ + char line[DBLKSIZ], value[DBLKSIZ]; + FILE *fp; + DBM *db; + char *cp, *vp; + datum key, val; + int len, cont, err = 0; + + fp = open_file (temp, "r"); + if ((db = dbm_open (temp, O_RDWR | O_CREAT | O_TRUNC, 0666)) == NULL) + error (1, errno, "cannot open dbm file %s for creation", temp); + for (cont = 0; fgets (line, sizeof (line), fp) != NULL;) + { + if ((cp = strrchr (line, '\n')) != NULL) + *cp = '\0'; /* strip the newline */ + + /* + * Add the line to the value, at the end if this is a continuation + * line; otherwise at the beginning, but only after any trailing + * backslash is removed. + */ + vp = value; + if (cont) + vp += strlen (value); + + /* + * See if the line we read is a continuation line, and strip the + * backslash if so. + */ + len = strlen (line); + if (len > 0) + cp = &line[len - 1]; + else + cp = line; + if (*cp == '\\') + { + cont = 1; + *cp = '\0'; + } + else + { + cont = 0; + } + (void) strcpy (vp, line); + if (value[0] == '#') + continue; /* comment line */ + vp = value; + while (*vp && isspace ((unsigned char) *vp)) + vp++; + if (*vp == '\0') + continue; /* empty line */ + + /* + * If this was not a continuation line, add the entry to the database + */ + if (!cont) + { + key.dptr = vp; + while (*vp && !isspace ((unsigned char) *vp)) + vp++; + key.dsize = vp - key.dptr; + *vp++ = '\0'; /* NULL terminate the key */ + while (*vp && isspace ((unsigned char) *vp)) + vp++; /* skip whitespace to value */ + if (*vp == '\0') + { + error (0, 0, "warning: NULL value for key `%s'", key.dptr); + continue; + } + val.dptr = vp; + val.dsize = strlen (vp); + if (dbm_store (db, key, val, DBM_INSERT) == 1) + { + error (0, 0, "duplicate key found for `%s'", key.dptr); + err++; + } + } + } + dbm_close (db); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", temp); + if (err) + { + /* I think that the size of the buffer needed here is + just determined by sizeof (CVSROOTADM_MODULES), the + filenames created by make_tempfile, and other things that won't + overflow. */ + char dotdir[50], dotpag[50], dotdb[50]; + + (void) sprintf (dotdir, "%s.dir", temp); + (void) sprintf (dotpag, "%s.pag", temp); + (void) sprintf (dotdb, "%s.db", temp); + if (unlink_file (dotdir) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", dotdir); + if (unlink_file (dotpag) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", dotpag); + if (unlink_file (dotdb) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", dotdb); + error (1, 0, "DBM creation failed; correct above errors"); + } +} + +static void +rename_dbmfile (temp) + char *temp; +{ + /* I think that the size of the buffer needed here is + just determined by sizeof (CVSROOTADM_MODULES), the + filenames created by make_tempfile, and other things that won't + overflow. */ + char newdir[50], newpag[50], newdb[50]; + char dotdir[50], dotpag[50], dotdb[50]; + char bakdir[50], bakpag[50], bakdb[50]; + + int dir1_errno = 0, pag1_errno = 0, db1_errno = 0; + int dir2_errno = 0, pag2_errno = 0, db2_errno = 0; + int dir3_errno = 0, pag3_errno = 0, db3_errno = 0; + + (void) sprintf (dotdir, "%s.dir", CVSROOTADM_MODULES); + (void) sprintf (dotpag, "%s.pag", CVSROOTADM_MODULES); + (void) sprintf (dotdb, "%s.db", CVSROOTADM_MODULES); + (void) sprintf (bakdir, "%s%s.dir", BAKPREFIX, CVSROOTADM_MODULES); + (void) sprintf (bakpag, "%s%s.pag", BAKPREFIX, CVSROOTADM_MODULES); + (void) sprintf (bakdb, "%s%s.db", BAKPREFIX, CVSROOTADM_MODULES); + (void) sprintf (newdir, "%s.dir", temp); + (void) sprintf (newpag, "%s.pag", temp); + (void) sprintf (newdb, "%s.db", temp); + + (void) chmod (newdir, 0666); + (void) chmod (newpag, 0666); + (void) chmod (newdb, 0666); + + /* don't mess with me */ + SIG_beginCrSect (); + + /* rm .#modules.dir .#modules.pag */ + if (unlink_file (bakdir) < 0) + dir1_errno = errno; + if (unlink_file (bakpag) < 0) + pag1_errno = errno; + if (unlink_file (bakdb) < 0) + db1_errno = errno; + + /* mv modules.dir .#modules.dir */ + if (CVS_RENAME (dotdir, bakdir) < 0) + dir2_errno = errno; + /* mv modules.pag .#modules.pag */ + if (CVS_RENAME (dotpag, bakpag) < 0) + pag2_errno = errno; + /* mv modules.db .#modules.db */ + if (CVS_RENAME (dotdb, bakdb) < 0) + db2_errno = errno; + + /* mv "temp".dir modules.dir */ + if (CVS_RENAME (newdir, dotdir) < 0) + dir3_errno = errno; + /* mv "temp".pag modules.pag */ + if (CVS_RENAME (newpag, dotpag) < 0) + pag3_errno = errno; + /* mv "temp".db modules.db */ + if (CVS_RENAME (newdb, dotdb) < 0) + db3_errno = errno; + + /* OK -- make my day */ + SIG_endCrSect (); + + /* I didn't want to call error() when we had signals blocked + (unnecessary?), but do it now. */ + if (dir1_errno && !existence_error (dir1_errno)) + error (0, dir1_errno, "cannot remove %s", bakdir); + if (pag1_errno && !existence_error (pag1_errno)) + error (0, pag1_errno, "cannot remove %s", bakpag); + if (db1_errno && !existence_error (db1_errno)) + error (0, db1_errno, "cannot remove %s", bakdb); + + if (dir2_errno && !existence_error (dir2_errno)) + error (0, dir2_errno, "cannot remove %s", bakdir); + if (pag2_errno && !existence_error (pag2_errno)) + error (0, pag2_errno, "cannot remove %s", bakpag); + if (db2_errno && !existence_error (db2_errno)) + error (0, db2_errno, "cannot remove %s", bakdb); + + if (dir3_errno && !existence_error (dir3_errno)) + error (0, dir3_errno, "cannot remove %s", bakdir); + if (pag3_errno && !existence_error (pag3_errno)) + error (0, pag3_errno, "cannot remove %s", bakpag); + if (db3_errno && !existence_error (db3_errno)) + error (0, db3_errno, "cannot remove %s", bakdb); +} + +#endif /* !MY_NDBM */ + +static void +rename_rcsfile (temp, real) + char *temp; + char *real; +{ + char *bak; + struct stat statbuf; + char *rcs; + + /* Set "x" bits if set in original. */ + rcs = xmalloc (strlen (real) + sizeof (RCSEXT) + 10); + (void) sprintf (rcs, "%s%s", real, RCSEXT); + statbuf.st_mode = 0; /* in case rcs file doesn't exist, but it should... */ + if (CVS_STAT (rcs, &statbuf) < 0 + && !existence_error (errno)) + error (0, errno, "cannot stat %s", rcs); + free (rcs); + + if (chmod (temp, 0444 | (statbuf.st_mode & 0111)) < 0) + error (0, errno, "warning: cannot chmod %s", temp); + bak = xmalloc (strlen (real) + sizeof (BAKPREFIX) + 10); + (void) sprintf (bak, "%s%s", BAKPREFIX, real); + + /* rm .#loginfo */ + if (unlink_file (bak) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", bak); + + /* mv loginfo .#loginfo */ + if (CVS_RENAME (real, bak) < 0 + && !existence_error (errno)) + error (0, errno, "cannot rename %s to %s", real, bak); + + /* mv "temp" loginfo */ + if (CVS_RENAME (temp, real) < 0 + && !existence_error (errno)) + error (0, errno, "cannot rename %s to %s", temp, real); + + free (bak); +} + +const char *const init_usage[] = { + "Usage: %s %s\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +init (argc, argv) + int argc; + char **argv; +{ + /* Name of CVSROOT directory. */ + char *adm; + /* Name of this administrative file. */ + char *info; + /* Name of ,v file for this administrative file. */ + char *info_v; + /* Exit status. */ + int err; + + const struct admin_file *fileptr; + + umask (cvsumask); + + if (argc == -1 || argc > 1) + usage (init_usage); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + + ign_setup (); + send_init_command (); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + /* Note: we do *not* create parent directories as needed like the + old cvsinit.sh script did. Few utilities do that, and a + non-existent parent directory is as likely to be a typo as something + which needs to be created. */ + mkdir_if_needed (CVSroot_directory); + + adm = xmalloc (strlen (CVSroot_directory) + sizeof (CVSROOTADM) + 10); + strcpy (adm, CVSroot_directory); + strcat (adm, "/"); + strcat (adm, CVSROOTADM); + mkdir_if_needed (adm); + + /* This is needed because we pass "fileptr->filename" not "info" + to add_rcs_file below. I think this would be easy to change, + thus nuking the need for CVS_CHDIR here, but I haven't looked + closely (e.g. see wrappers calls within add_rcs_file). */ + if ( CVS_CHDIR (adm) < 0) + error (1, errno, "cannot change to directory %s", adm); + + /* Make Emptydir so it's there if we need it */ + make_directory (CVSNULLREPOS); + + /* 80 is long enough for all the administrative file names, plus + "/" and so on. */ + info = xmalloc (strlen (adm) + 80); + info_v = xmalloc (strlen (adm) + 80); + for (fileptr = filelist; fileptr && fileptr->filename; ++fileptr) + { + if (fileptr->contents == NULL) + continue; + strcpy (info, adm); + strcat (info, "/"); + strcat (info, fileptr->filename); + strcpy (info_v, info); + strcat (info_v, RCSEXT); + if (isfile (info_v)) + /* We will check out this file in the mkmodules step. + Nothing else is required. */ + ; + else + { + int retcode; + + if (!isfile (info)) + { + FILE *fp; + const char * const *p; + + fp = open_file (info, "w"); + for (p = fileptr->contents; *p != NULL; ++p) + if (fputs (*p, fp) < 0) + error (1, errno, "cannot write %s", info); + if (fclose (fp) < 0) + error (1, errno, "cannot close %s", info); + } + /* The message used to say " of " and fileptr->filename after + "initial checkin" but I fail to see the point as we know what + file it is from the name. */ + retcode = add_rcs_file ("initial checkin", info_v, + fileptr->filename, "1.1", NULL, + + /* No vendor branch. */ + NULL, NULL, 0, NULL, + + NULL, 0, NULL); + if (retcode != 0) + /* add_rcs_file already printed an error message. */ + err = 1; + } + } + + /* Turn on history logging by default. The user can remove the file + to disable it. */ + strcpy (info, adm); + strcat (info, "/"); + strcat (info, CVSROOTADM_HISTORY); + if (!isfile (info)) + { + FILE *fp; + + fp = open_file (info, "w"); + if (fclose (fp) < 0) + error (1, errno, "cannot close %s", info); + + /* Make the new history file world-writeable, since every CVS + user will need to be able to write to it. We use chmod() + because xchmod() is too shy. */ + chmod (info, 0666); + } + + free (info); + free (info_v); + + mkmodules (adm); + + free (adm); + return 0; +} diff --git a/gnu/dist/cvs/src/modules.c b/gnu/dist/cvs/src/modules.c new file mode 100644 index 00000000000..78bc1631660 --- /dev/null +++ b/gnu/dist/cvs/src/modules.c @@ -0,0 +1,1074 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License + * as specified in the README file that comes with the CVS source distribution. + * + * Modules + * + * Functions for accessing the modules file. + * + * The modules file supports basically three formats of lines: + * key [options] directory files... [ -x directory [files] ] ... + * key [options] directory [ -x directory [files] ] ... + * key -a aliases... + * + * The -a option allows an aliasing step in the parsing of the modules + * file. The "aliases" listed on a line following the -a are + * processed one-by-one, as if they were specified as arguments on the + * command line. + */ + +#include <assert.h> +#include "cvs.h" +#include "savecwd.h" + + +/* Defines related to the syntax of the modules file. */ + +/* Options in modules file. Note that it is OK to use GNU getopt features; + we already are arranging to make sure we are using the getopt distributed + with CVS. */ +#define CVSMODULE_OPTS "+ad:i:lo:e:s:t:u:" + +/* Special delimiter. */ +#define CVSMODULE_SPEC '&' + +struct sortrec +{ + /* Name of the module, malloc'd. */ + char *modname; + /* If Status variable is set, this is either def_status or the malloc'd + name of the status. If Status is not set, the field is left + uninitialized. */ + char *status; + /* Pointer to a malloc'd array which contains (1) the raw contents + of the options and arguments, excluding comments, (2) a '\0', + and (3) the storage for the "comment" field. */ + char *rest; + char *comment; +}; + +static int sort_order PROTO((const PTR l, const PTR r)); +static void save_d PROTO((char *k, int ks, char *d, int ds)); + + +/* + * Open the modules file, and die if the CVSROOT environment variable + * was not set. If the modules file does not exist, that's fine, and + * a warning message is displayed and a NULL is returned. + */ +DBM * +open_module () +{ + char *mfile; + DBM *retval; + + if (CVSroot_original == NULL) + { + error (0, 0, "must set the CVSROOT environment variable"); + error (1, 0, "or specify the '-d' global option"); + } + mfile = xmalloc (strlen (CVSroot_directory) + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_MODULES) + 20); + (void) sprintf (mfile, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_MODULES); + retval = dbm_open (mfile, O_RDONLY, 0666); + free (mfile); + return retval; +} + +/* + * Close the modules file, if the open succeeded, that is + */ +void +close_module (db) + DBM *db; +{ + if (db != NULL) + dbm_close (db); +} + +/* + * This is the recursive function that processes a module name. + * It calls back the passed routine for each directory of a module + * It runs the post checkout or post tag proc from the modules file + */ +int +do_module (db, mname, m_type, msg, callback_proc, where, + shorten, local_specified, run_module_prog, extra_arg) + DBM *db; + char *mname; + enum mtype m_type; + char *msg; + CALLBACKPROC callback_proc; + char *where; + int shorten; + int local_specified; + int run_module_prog; + char *extra_arg; +{ + char *checkin_prog = NULL; + char *checkout_prog = NULL; + char *export_prog = NULL; + char *tag_prog = NULL; + char *update_prog = NULL; + struct saved_cwd cwd; + int cwd_saved = 0; + char *line; + int modargc; + int xmodargc; + char **modargv; + char **xmodargv; + /* Found entry from modules file, including options and such. */ + char *value = NULL; + char *zvalue = NULL; + char *mwhere = NULL; + char *mfile = NULL; + char *spec_opt = NULL; + char *xvalue = NULL; + int alias = 0; + datum key, val; + char *cp; + int c, err = 0; + int nonalias_opt = 0; + +#ifdef SERVER_SUPPORT + int restore_server_dir = 0; + char *server_dir_to_restore = NULL; + if (trace) + { + char *buf; + + /* We use cvs_outerr, rather than fprintf to stderr, because + this may be called by server code with error_use_protocol + set. */ + buf = xmalloc (100 + + strlen (mname) + + strlen (msg) + + (where ? strlen (where) : 0) + + (extra_arg ? strlen (extra_arg) : 0)); + sprintf (buf, "%s-> do_module (%s, %s, %s, %s)\n", + CLIENT_SERVER_STR, + mname, msg, where ? where : "", + extra_arg ? extra_arg : ""); + cvs_outerr (buf, 0); + free (buf); + } +#endif + + /* if this is a directory to ignore, add it to that list */ + if (mname[0] == '!' && mname[1] != '\0') + { + ign_dir_add (mname+1); + goto do_module_return; + } + + /* strip extra stuff from the module name */ + strip_trailing_slashes (mname); + + /* + * Look up the module using the following scheme: + * 1) look for mname as a module name + * 2) look for mname as a directory + * 3) look for mname as a file + * 4) take mname up to the first slash and look it up as a module name + * (this is for checking out only part of a module) + */ + + /* look it up as a module name */ + key.dptr = mname; + key.dsize = strlen (key.dptr); + if (db != NULL) + val = dbm_fetch (db, key); + else + val.dptr = NULL; + if (val.dptr != NULL) + { + /* null terminate the value XXX - is this space ours? */ + val.dptr[val.dsize] = '\0'; + + /* If the line ends in a comment, strip it off */ + if ((cp = strchr (val.dptr, '#')) != NULL) + *cp = '\0'; + else + cp = val.dptr + val.dsize; + + /* Always strip trailing spaces */ + while (cp > val.dptr && isspace ((unsigned char) *--cp)) + *cp = '\0'; + + value = val.dptr; + mwhere = xstrdup (mname); + goto found; + } + else + { + char *file; + char *attic_file; + char *acp; + int is_found = 0; + + /* check to see if mname is a directory or file */ + file = xmalloc (strlen (CVSroot_directory) + strlen (mname) + 10); + (void) sprintf (file, "%s/%s", CVSroot_directory, mname); + attic_file = xmalloc (strlen (CVSroot_directory) + strlen (mname) + + sizeof (CVSATTIC) + sizeof (RCSEXT) + 15); + if ((acp = strrchr (mname, '/')) != NULL) + { + *acp = '\0'; + (void) sprintf (attic_file, "%s/%s/%s/%s%s", CVSroot_directory, + mname, CVSATTIC, acp + 1, RCSEXT); + *acp = '/'; + } + else + (void) sprintf (attic_file, "%s/%s/%s%s", CVSroot_directory, + CVSATTIC, mname, RCSEXT); + + if (isdir (file)) + { + modargv = xmalloc (sizeof (*modargv)); + modargv[0] = xstrdup (mname); + modargc = 1; + is_found = 1; + } + else + { + (void) strcat (file, RCSEXT); + if (isfile (file) || isfile (attic_file)) + { + /* if mname was a file, we have to split it into "dir file" */ + if ((cp = strrchr (mname, '/')) != NULL && cp != mname) + { + modargv = xmalloc (2 * sizeof (*modargv)); + modargv[0] = xmalloc (strlen (mname) + 2); + strncpy (modargv[0], mname, cp - mname); + modargv[0][cp - mname] = '\0'; + modargv[1] = xstrdup (cp + 1); + modargc = 2; + } + else + { + /* + * the only '/' at the beginning or no '/' at all + * means the file we are interested in is in CVSROOT + * itself so the directory should be '.' + */ + if (cp == mname) + { + /* drop the leading / if specified */ + modargv = xmalloc (2 * sizeof (*modargv)); + modargv[0] = xstrdup ("."); + modargv[1] = xstrdup (mname + 1); + modargc = 2; + } + else + { + /* otherwise just copy it */ + modargv = xmalloc (2 * sizeof (*modargv)); + modargv[0] = xstrdup ("."); + modargv[1] = xstrdup (mname); + modargc = 2; + } + } + is_found = 1; + } + } + free (attic_file); + free (file); + + if (is_found) + { + assert (value == NULL); + + /* OK, we have now set up modargv with the actual + file/directory we want to work on. We duplicate a + small amount of code here because the vast majority of + the code after the "found" label does not pertain to + the case where we found a file/directory rather than + finding an entry in the modules file. */ + if (save_cwd (&cwd)) + error_exit (); + cwd_saved = 1; + + err += callback_proc (&modargc, modargv, where, mwhere, mfile, + shorten, + local_specified, mname, msg); + + free_names (&modargc, modargv); + + /* cd back to where we started. */ + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + cwd_saved = 0; + + goto do_module_return; + } + } + + /* look up everything to the first / as a module */ + if (mname[0] != '/' && (cp = strchr (mname, '/')) != NULL) + { + /* Make the slash the new end of the string temporarily */ + *cp = '\0'; + key.dptr = mname; + key.dsize = strlen (key.dptr); + + /* do the lookup */ + if (db != NULL) + val = dbm_fetch (db, key); + else + val.dptr = NULL; + + /* if we found it, clean up the value and life is good */ + if (val.dptr != NULL) + { + char *cp2; + + /* null terminate the value XXX - is this space ours? */ + val.dptr[val.dsize] = '\0'; + + /* If the line ends in a comment, strip it off */ + if ((cp2 = strchr (val.dptr, '#')) != NULL) + *cp2 = '\0'; + else + cp2 = val.dptr + val.dsize; + + /* Always strip trailing spaces */ + while (cp2 > val.dptr && isspace ((unsigned char) *--cp2)) + *cp2 = '\0'; + + value = val.dptr; + + /* mwhere gets just the module name */ + mwhere = xstrdup (mname); + mfile = cp + 1; + + /* put the / back in mname */ + *cp = '/'; + + goto found; + } + + /* put the / back in mname */ + *cp = '/'; + } + + /* if we got here, we couldn't find it using our search, so give up */ + error (0, 0, "cannot find module `%s' - ignored", mname); + err++; + goto do_module_return; + + + /* + * At this point, we found what we were looking for in one + * of the many different forms. + */ + found: + + /* remember where we start */ + if (save_cwd (&cwd)) + error_exit (); + cwd_saved = 1; + + /* copy value to our own string since if we go recursive we'll be + really screwed if we do another dbm lookup */ + assert (value != NULL); + zvalue = xstrdup (value); + value = zvalue; + + /* search the value for the special delimiter and save for later */ + if ((cp = strchr (value, CVSMODULE_SPEC)) != NULL) + { + *cp = '\0'; /* null out the special char */ + spec_opt = cp + 1; /* save the options for later */ + + /* strip whitespace if necessary */ + while (cp > value && isspace ((unsigned char) *--cp)) + *cp = '\0'; + } + + /* don't do special options only part of a module was specified */ + if (mfile != NULL) + spec_opt = NULL; + + /* + * value now contains one of the following: + * 1) dir + * 2) dir file + * 3) the value from modules without any special args + * [ args ] dir [file] [file] ... + * or -a module [ module ] ... + */ + + /* Put the value on a line with XXX prepended for getopt to eat */ + line = xmalloc (strlen (value) + 10); + strcpy(line, "XXX "); + strcpy(line + 4, value); + + /* turn the line into an argv[] array */ + line2argv (&xmodargc, &xmodargv, line, " \t"); + free (line); + modargc = xmodargc; + modargv = xmodargv; + + /* parse the args */ + optind = 0; + while ((c = getopt (modargc, modargv, CVSMODULE_OPTS)) != -1) + { + switch (c) + { + case 'a': + alias = 1; + break; + case 'd': + if (mwhere) + free (mwhere); + mwhere = xstrdup (optarg); + nonalias_opt = 1; + break; + case 'i': + if (checkin_prog) + free (checkin_prog); + checkin_prog = xstrdup (optarg); + nonalias_opt = 1; + break; + case 'l': + local_specified = 1; + nonalias_opt = 1; + break; + case 'o': + if (checkout_prog) + free (checkout_prog); + checkout_prog = xstrdup (optarg); + nonalias_opt = 1; + break; + case 'e': + if (export_prog) + free (export_prog); + export_prog = xstrdup (optarg); + nonalias_opt = 1; + break; + case 't': + if (tag_prog) + free (tag_prog); + tag_prog = xstrdup (optarg); + nonalias_opt = 1; + break; + case 'u': + if (update_prog) + free (update_prog); + update_prog = xstrdup (optarg); + nonalias_opt = 1; + break; + case '?': + error (0, 0, + "modules file has invalid option for key %s value %s", + key.dptr, val.dptr); + err++; + goto do_module_return; + } + } + modargc -= optind; + modargv += optind; + if (modargc == 0 && spec_opt == NULL) + { + error (0, 0, "modules file missing directory for module %s", mname); + ++err; + goto do_module_return; + } + + if (alias && nonalias_opt) + { + /* The documentation has never said it is legal to specify + -a along with another option. And I believe that in the past + CVS has ignored the options other than -a, more or less, in this + situation. */ + error (0, 0, "\ +-a cannot be specified in the modules file along with other options"); + ++err; + goto do_module_return; + } + + /* if this was an alias, call ourselves recursively for each module */ + if (alias) + { + int i; + + for (i = 0; i < modargc; i++) + { + if (strcmp (mname, modargv[i]) == 0) + error (0, 0, + "module `%s' in modules file contains infinite loop", + mname); + else + err += do_module (db, modargv[i], m_type, msg, callback_proc, + where, shorten, local_specified, + run_module_prog, extra_arg); + } + goto do_module_return; + } + + if (mfile != NULL && modargc > 1) + { + error (0, 0, "\ +module `%s' is a request for a file in a module which is not a directory", + mname); + ++err; + goto do_module_return; + } + + /* otherwise, process this module */ + if (modargc > 0) + { + err += callback_proc (&modargc, modargv, where, mwhere, mfile, shorten, + local_specified, mname, msg); + } + else + { + /* + * we had nothing but special options, so we must + * make the appropriate directory and cd to it + */ + char *dir; + + /* XXX - XXX - MAJOR HACK - DO NOT SHIP - this needs to + be !pipeout, but we don't know that here yet */ + if (!run_module_prog) + goto do_special; + + dir = where ? where : (mwhere ? mwhere : mname); + /* XXX - think about making null repositories at each dir here + instead of just at the bottom */ + make_directories (dir); + if ( CVS_CHDIR (dir) < 0) + { + error (0, errno, "cannot chdir to %s", dir); + spec_opt = NULL; + err++; + goto do_special; + } + if (!isfile (CVSADM)) + { + char *nullrepos; + + nullrepos = emptydir_name (); + + Create_Admin (".", dir, + nullrepos, (char *) NULL, (char *) NULL, 0, 0); + if (!noexec) + { + FILE *fp; + + fp = open_file (CVSADM_ENTSTAT, "w+"); + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", CVSADM_ENTSTAT); +#ifdef SERVER_SUPPORT + if (server_active) + server_set_entstat (dir, nullrepos); +#endif + } + free (nullrepos); + } + } + + /* if there were special include args, process them now */ + + do_special: + + free_names (&xmodargc, xmodargv); + + /* blow off special options if -l was specified */ + if (local_specified) + spec_opt = NULL; + +#ifdef SERVER_SUPPORT + /* We want to check out into the directory named by the module. + So we set a global variable which tells the server to glom that + directory name onto the front. A cleaner approach would be some + way of passing it down to the recursive call, through the + callback_proc, to start_recursion, and then into the update_dir in + the struct file_info. That way the "Updating foo" message could + print the actual directory we are checking out into. + + For local CVS, this is handled by the chdir call above + (directly or via the callback_proc). */ + if (server_active && spec_opt != NULL) + { + char *change_to; + + change_to = where ? where : (mwhere ? mwhere : mname); + server_dir_to_restore = server_dir; + restore_server_dir = 1; + server_dir = + xmalloc ((server_dir_to_restore != NULL + ? strlen (server_dir_to_restore) + : 0) + + strlen (change_to) + + 5); + server_dir[0] = '\0'; + if (server_dir_to_restore != NULL) + { + strcat (server_dir, server_dir_to_restore); + strcat (server_dir, "/"); + } + strcat (server_dir, change_to); + } +#endif + + while (spec_opt != NULL) + { + char *next_opt; + + cp = strchr (spec_opt, CVSMODULE_SPEC); + if (cp != NULL) + { + /* save the beginning of the next arg */ + next_opt = cp + 1; + + /* strip whitespace off the end */ + do + *cp = '\0'; + while (cp > spec_opt && isspace ((unsigned char) *--cp)); + } + else + next_opt = NULL; + + /* strip whitespace from front */ + while (isspace ((unsigned char) *spec_opt)) + spec_opt++; + + if (*spec_opt == '\0') + error (0, 0, "Mal-formed %c option for module %s - ignored", + CVSMODULE_SPEC, mname); + else + err += do_module (db, spec_opt, m_type, msg, callback_proc, + (char *) NULL, 0, local_specified, + run_module_prog, extra_arg); + spec_opt = next_opt; + } + +#ifdef SERVER_SUPPORT + if (server_active && restore_server_dir) + { + free (server_dir); + server_dir = server_dir_to_restore; + } +#endif + + /* write out the checkin/update prog files if necessary */ +#ifdef SERVER_SUPPORT + if (err == 0 && !noexec && m_type == CHECKOUT && server_expanding) + { + if (checkin_prog != NULL) + server_prog (where ? where : mname, checkin_prog, PROG_CHECKIN); + if (update_prog != NULL) + server_prog (where ? where : mname, update_prog, PROG_UPDATE); + } + else +#endif + if (err == 0 && !noexec && m_type == CHECKOUT && run_module_prog) + { + FILE *fp; + + if (checkin_prog != NULL) + { + fp = open_file (CVSADM_CIPROG, "w+"); + (void) fprintf (fp, "%s\n", checkin_prog); + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", CVSADM_CIPROG); + } + if (update_prog != NULL) + { + fp = open_file (CVSADM_UPROG, "w+"); + (void) fprintf (fp, "%s\n", update_prog); + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", CVSADM_UPROG); + } + } + + /* cd back to where we started */ + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + cwd_saved = 0; + + /* run checkout or tag prog if appropriate */ + if (err == 0 && run_module_prog) + { + if ((m_type == TAG && tag_prog != NULL) || + (m_type == CHECKOUT && checkout_prog != NULL) || + (m_type == EXPORT && export_prog != NULL)) + { + /* + * If a relative pathname is specified as the checkout, tag + * or export proc, try to tack on the current "where" value. + * if we can't find a matching program, just punt and use + * whatever is specified in the modules file. + */ + char *real_prog = NULL; + char *prog = (m_type == TAG ? tag_prog : + (m_type == CHECKOUT ? checkout_prog : export_prog)); + char *real_where = (where != NULL ? where : mwhere); + char *expanded_path; + + if ((*prog != '/') && (*prog != '.')) + { + real_prog = xmalloc (strlen (real_where) + strlen (prog) + + 10); + (void) sprintf (real_prog, "%s/%s", real_where, prog); + if (isfile (real_prog)) + prog = real_prog; + } + + /* XXX can we determine the line number for this entry??? */ + expanded_path = expand_path (prog, "modules", 0); + if (expanded_path != NULL) + { + run_setup (expanded_path); + run_arg (real_where); + + if (extra_arg) + run_arg (extra_arg); + + if (!quiet) + { + cvs_output (program_name, 0); + cvs_output (" ", 1); + cvs_output (command_name, 0); + cvs_output (": Executing '", 0); + run_print (stdout); + cvs_output ("'\n", 0); + } + err += run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL); + free (expanded_path); + } + free (real_prog); + } + } + + do_module_return: + /* clean up */ + if (mwhere) + free (mwhere); + if (checkin_prog) + free (checkin_prog); + if (checkout_prog) + free (checkout_prog); + if (export_prog) + free (export_prog); + if (tag_prog) + free (tag_prog); + if (update_prog) + free (update_prog); + if (cwd_saved) + free_cwd (&cwd); + if (zvalue != NULL) + free (zvalue); + + if (xvalue != NULL) + free (xvalue); + return (err); +} + +/* - Read all the records from the modules database into an array. + - Sort the array depending on what format is desired. + - Print the array in the format desired. + + Currently, there are only two "desires": + + 1. Sort by module name and format the whole entry including switches, + files and the comment field: (Including aliases) + + modulename -s switches, one per line, even if + -i it has many switches. + Directories and files involved, formatted + to cover multiple lines if necessary. + # Comment, also formatted to cover multiple + # lines if necessary. + + 2. Sort by status field string and print: (*not* including aliases) + + modulename STATUS Directories and files involved, formatted + to cover multiple lines if necessary. + # Comment, also formatted to cover multiple + # lines if necessary. +*/ + +static struct sortrec *s_head; + +static int s_max = 0; /* Number of elements allocated */ +static int s_count = 0; /* Number of elements used */ + +static int Status; /* Nonzero if the user is + interested in status + information as well as + module name */ +static char def_status[] = "NONE"; + +/* Sort routine for qsort: + - If we want the "Status" field to be sorted, check it first. + - Then compare the "module name" fields. Since they are unique, we don't + have to look further. +*/ +static int +sort_order (l, r) + const PTR l; + const PTR r; +{ + int i; + const struct sortrec *left = (const struct sortrec *) l; + const struct sortrec *right = (const struct sortrec *) r; + + if (Status) + { + /* If Sort by status field, compare them. */ + if ((i = strcmp (left->status, right->status)) != 0) + return (i); + } + return (strcmp (left->modname, right->modname)); +} + +static void +save_d (k, ks, d, ds) + char *k; + int ks; + char *d; + int ds; +{ + char *cp, *cp2; + struct sortrec *s_rec; + + if (Status && *d == '-' && *(d + 1) == 'a') + return; /* We want "cvs co -s" and it is an alias! */ + + if (s_count == s_max) + { + s_max += 64; + s_head = (struct sortrec *) xrealloc ((char *) s_head, s_max * sizeof (*s_head)); + } + s_rec = &s_head[s_count]; + s_rec->modname = cp = xmalloc (ks + 1); + (void) strncpy (cp, k, ks); + *(cp + ks) = '\0'; + + s_rec->rest = cp2 = xmalloc (ds + 1); + cp = d; + *(cp + ds) = '\0'; /* Assumes an extra byte at end of static dbm buffer */ + + while (isspace ((unsigned char) *cp)) + cp++; + /* Turn <spaces> into one ' ' -- makes the rest of this routine simpler */ + while (*cp) + { + if (isspace ((unsigned char) *cp)) + { + *cp2++ = ' '; + while (isspace ((unsigned char) *cp)) + cp++; + } + else + *cp2++ = *cp++; + } + *cp2 = '\0'; + + /* Look for the "-s statusvalue" text */ + if (Status) + { + s_rec->status = def_status; + + for (cp = s_rec->rest; (cp2 = strchr (cp, '-')) != NULL; cp = ++cp2) + { + if (*(cp2 + 1) == 's' && *(cp2 + 2) == ' ') + { + char *status_start; + + cp2 += 3; + status_start = cp2; + while (*cp2 != ' ' && *cp2 != '\0') + cp2++; + s_rec->status = xmalloc (cp2 - status_start + 1); + strncpy (s_rec->status, status_start, cp2 - status_start); + s_rec->status[cp2 - status_start] = '\0'; + cp = cp2; + break; + } + } + } + else + cp = s_rec->rest; + + /* Find comment field, clean up on all three sides & compress blanks */ + if ((cp2 = cp = strchr (cp, '#')) != NULL) + { + if (*--cp2 == ' ') + *cp2 = '\0'; + if (*++cp == ' ') + cp++; + s_rec->comment = cp; + } + else + s_rec->comment = ""; + + s_count++; +} + +/* Print out the module database as we know it. If STATUS is + non-zero, print out status information for each module. */ + +void +cat_module (status) + int status; +{ + DBM *db; + datum key, val; + int i, c, wid, argc, cols = 80, indent, fill; + int moduleargc; + struct sortrec *s_h; + char *cp, *cp2, **argv; + char **moduleargv; + + Status = status; + + /* Read the whole modules file into allocated records */ + if (!(db = open_module ())) + error (1, 0, "failed to open the modules file"); + + for (key = dbm_firstkey (db); key.dptr != NULL; key = dbm_nextkey (db)) + { + val = dbm_fetch (db, key); + if (val.dptr != NULL) + save_d (key.dptr, key.dsize, val.dptr, val.dsize); + } + + close_module (db); + + /* Sort the list as requested */ + qsort ((PTR) s_head, s_count, sizeof (struct sortrec), sort_order); + + /* + * Run through the sorted array and format the entries + * indent = space for modulename + space for status field + */ + indent = 12 + (status * 12); + fill = cols - (indent + 2); + for (s_h = s_head, i = 0; i < s_count; i++, s_h++) + { + char *line; + + /* Print module name (and status, if wanted) */ + line = xmalloc (strlen (s_h->modname) + 15); + sprintf (line, "%-12s", s_h->modname); + cvs_output (line, 0); + free (line); + if (status) + { + line = xmalloc (strlen (s_h->status) + 15); + sprintf (line, " %-11s", s_h->status); + cvs_output (line, 0); + free (line); + } + + line = xmalloc (strlen (s_h->modname) + strlen (s_h->rest) + 15); + /* Parse module file entry as command line and print options */ + (void) sprintf (line, "%s %s", s_h->modname, s_h->rest); + line2argv (&moduleargc, &moduleargv, line, " \t"); + free (line); + argc = moduleargc; + argv = moduleargv; + + optind = 0; + wid = 0; + while ((c = getopt (argc, argv, CVSMODULE_OPTS)) != -1) + { + if (!status) + { + if (c == 'a' || c == 'l') + { + char buf[5]; + + sprintf (buf, " -%c", c); + cvs_output (buf, 0); + wid += 3; /* Could just set it to 3 */ + } + else + { + char buf[10]; + + if (strlen (optarg) + 4 + wid > (unsigned) fill) + { + int j; + + cvs_output ("\n", 1); + for (j = 0; j < indent; ++j) + cvs_output (" ", 1); + wid = 0; + } + sprintf (buf, " -%c ", c); + cvs_output (buf, 0); + cvs_output (optarg, 0); + wid += strlen (optarg) + 4; + } + } + } + argc -= optind; + argv += optind; + + /* Format and Print all the files and directories */ + for (; argc--; argv++) + { + if (strlen (*argv) + wid > (unsigned) fill) + { + int j; + + cvs_output ("\n", 1); + for (j = 0; j < indent; ++j) + cvs_output (" ", 1); + wid = 0; + } + cvs_output (" ", 1); + cvs_output (*argv, 0); + wid += strlen (*argv) + 1; + } + cvs_output ("\n", 1); + + /* Format the comment field -- save_d (), compressed spaces */ + for (cp2 = cp = s_h->comment; *cp; cp2 = cp) + { + int j; + + for (j = 0; j < indent; ++j) + cvs_output (" ", 1); + cvs_output (" # ", 0); + if (strlen (cp2) < (unsigned) (fill - 2)) + { + cvs_output (cp2, 0); + cvs_output ("\n", 1); + break; + } + cp += fill - 2; + while (*cp != ' ' && cp > cp2) + cp--; + if (cp == cp2) + { + cvs_output (cp2, 0); + cvs_output ("\n", 1); + break; + } + + *cp++ = '\0'; + cvs_output (cp2, 0); + cvs_output ("\n", 1); + } + + free_names(&moduleargc, moduleargv); + /* FIXME-leak: here is where we would free s_h->modname, s_h->rest, + and if applicable, s_h->status. Not exactly a memory leak, + in the sense that we are about to exit(), but may be worth + noting if we ever do a multithreaded server or something of + the sort. */ + } + /* FIXME-leak: as above, here is where we would free s_head. */ +} diff --git a/gnu/dist/cvs/src/myndbm.c b/gnu/dist/cvs/src/myndbm.c new file mode 100644 index 00000000000..f674ac11d9f --- /dev/null +++ b/gnu/dist/cvs/src/myndbm.c @@ -0,0 +1,309 @@ +/* + * Copyright (c) 1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * A simple ndbm-emulator for CVS. It parses a text file of the format: + * + * key value + * + * at dbm_open time, and loads the entire file into memory. As such, it is + * probably only good for fairly small modules files. Ours is about 30K in + * size, and this code works fine. + */ + +#include <assert.h> +#include "cvs.h" +#include "getline.h" + +#ifdef MY_NDBM + +static void mydbm_load_file PROTO ((FILE *, List *)); + +/* Returns NULL on error in which case errno has been set to indicate + the error. Can also call error() itself. */ +/* ARGSUSED */ +DBM * +mydbm_open (file, flags, mode) + char *file; + int flags; + int mode; +{ + FILE *fp; + DBM *db; + + fp = CVS_FOPEN (file, FOPEN_BINARY_READ); + if (fp == NULL && !(existence_error (errno) && (flags & O_CREAT))) + return ((DBM *) 0); + + db = (DBM *) xmalloc (sizeof (*db)); + db->dbm_list = getlist (); + db->modified = 0; + db->name = xstrdup (file); + + if (fp != NULL) + { + mydbm_load_file (fp, db->dbm_list); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", file); + } + return (db); +} + +static int write_item PROTO ((Node *, void *)); + +static int +write_item (node, data) + Node *node; + void *data; +{ + FILE *fp = (FILE *)data; + fputs (node->key, fp); + fputs (" ", fp); + fputs (node->data, fp); + fputs ("\012", fp); + return 0; +} + +void +mydbm_close (db) + DBM *db; +{ + if (db->modified) + { + FILE *fp; + fp = CVS_FOPEN (db->name, FOPEN_BINARY_WRITE); + if (fp == NULL) + error (1, errno, "cannot write %s", db->name); + walklist (db->dbm_list, write_item, (void *)fp); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", db->name); + } + free (db->name); + dellist (&db->dbm_list); + free ((char *) db); +} + +datum +mydbm_fetch (db, key) + DBM *db; + datum key; +{ + Node *p; + char *s; + datum val; + + /* make sure it's null-terminated */ + s = xmalloc (key.dsize + 1); + (void) strncpy (s, key.dptr, key.dsize); + s[key.dsize] = '\0'; + + p = findnode (db->dbm_list, s); + if (p) + { + val.dptr = p->data; + val.dsize = strlen (p->data); + } + else + { + val.dptr = (char *) NULL; + val.dsize = 0; + } + free (s); + return (val); +} + +datum +mydbm_firstkey (db) + DBM *db; +{ + Node *head, *p; + datum key; + + head = db->dbm_list->list; + p = head->next; + if (p != head) + { + key.dptr = p->key; + key.dsize = strlen (p->key); + } + else + { + key.dptr = (char *) NULL; + key.dsize = 0; + } + db->dbm_next = p->next; + return (key); +} + +datum +mydbm_nextkey (db) + DBM *db; +{ + Node *head, *p; + datum key; + + head = db->dbm_list->list; + p = db->dbm_next; + if (p != head) + { + key.dptr = p->key; + key.dsize = strlen (p->key); + } + else + { + key.dptr = (char *) NULL; + key.dsize = 0; + } + db->dbm_next = p->next; + return (key); +} + +/* Note: only updates the in-memory copy, which is written out at + mydbm_close time. Note: Also differs from DBM in that on duplication, + it gives a warning, rather than either DBM_INSERT or DBM_REPLACE + behavior. */ +int +mydbm_store (db, key, value, flags) + DBM *db; + datum key; + datum value; + int flags; +{ + Node *node; + + node = getnode (); + node->type = NDBMNODE; + + node->key = xmalloc (key.dsize + 1); + strncpy (node->key, key.dptr, key.dsize); + node->key[key.dsize] = '\0'; + + node->data = xmalloc (value.dsize + 1); + strncpy (node->data, value.dptr, value.dsize); + node->data[value.dsize] = '\0'; + + db->modified = 1; + if (addnode (db->dbm_list, node) == -1) + { + error (0, 0, "attempt to insert duplicate key `%s'", node->key); + freenode (node); + return 0; + } + return 0; +} + +static void +mydbm_load_file (fp, list) + FILE *fp; + List *list; +{ + char *line = NULL; + size_t line_size; + char *value; + size_t value_allocated; + char *cp, *vp; + int cont; + int line_length; + + value_allocated = 1; + value = xmalloc (value_allocated); + + cont = 0; + while ((line_length = getstr (&line, &line_size, fp, '\012', 0)) >= 0) + { + if (line_length > 0 && line[line_length - 1] == '\012') + { + /* Strip the newline. */ + --line_length; + line[line_length] = '\0'; + } + if (line_length > 0 && line[line_length - 1] == '\015') + { + /* If the file (e.g. modules) was written on an NT box, it will + contain CRLF at the ends of lines. Strip them (we can't do + this by opening the file in text mode because we might be + running on unix). */ + --line_length; + line[line_length] = '\0'; + } + + /* + * Add the line to the value, at the end if this is a continuation + * line; otherwise at the beginning, but only after any trailing + * backslash is removed. + */ + if (!cont) + value[0] = '\0'; + + /* + * See if the line we read is a continuation line, and strip the + * backslash if so. + */ + if (line_length > 0) + cp = &line[line_length - 1]; + else + cp = line; + if (*cp == '\\') + { + cont = 1; + *cp = '\0'; + --line_length; + } + else + { + cont = 0; + } + expand_string (&value, + &value_allocated, + strlen (value) + line_length + 5); + strcat (value, line); + + if (value[0] == '#') + continue; /* comment line */ + vp = value; + while (*vp && isspace ((unsigned char) *vp)) + vp++; + if (*vp == '\0') + continue; /* empty line */ + + /* + * If this was not a continuation line, add the entry to the database + */ + if (!cont) + { + Node *p = getnode (); + char *kp; + + kp = vp; + while (*vp && !isspace ((unsigned char) *vp)) + vp++; + *vp++ = '\0'; /* NULL terminate the key */ + p->type = NDBMNODE; + p->key = xstrdup (kp); + while (*vp && isspace ((unsigned char) *vp)) + vp++; /* skip whitespace to value */ + if (*vp == '\0') + { + error (0, 0, "warning: NULL value for key `%s'", p->key); + freenode (p); + continue; + } + p->data = xstrdup (vp); + if (addnode (list, p) == -1) + { + error (0, 0, "duplicate key found for `%s'", p->key); + freenode (p); + } + } + } + if (line_length < 0 && !feof (fp)) + /* FIXME: should give the name of the file. */ + error (0, errno, "cannot read file in mydbm_load_file"); + + free (line); + free (value); +} + +#endif /* MY_NDBM */ diff --git a/gnu/dist/cvs/src/no_diff.c b/gnu/dist/cvs/src/no_diff.c new file mode 100644 index 00000000000..dfca372f5cb --- /dev/null +++ b/gnu/dist/cvs/src/no_diff.c @@ -0,0 +1,93 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * No Difference + * + * The user file looks modified judging from its time stamp; however it needn't + * be. No_difference() finds out whether it is or not. If it is not, it + * updates the administration. + * + * returns 0 if no differences are found and non-zero otherwise + */ + +#include "cvs.h" + +int +No_Difference (finfo, vers) + struct file_info *finfo; + Vers_TS *vers; +{ + Node *p; + int ret; + char *ts, *options; + int retcode = 0; + char *tocvsPath; + + /* If ts_user is "Is-modified", we can only conclude the files are + different (since we don't have the file's contents). */ + if (vers->ts_user != NULL + && strcmp (vers->ts_user, "Is-modified") == 0) + return -1; + + if (!vers->srcfile || !vers->srcfile->path) + return (-1); /* different since we couldn't tell */ + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* If special files are in use, then any mismatch of file metadata + information also means that the files should be considered different. */ + if (preserve_perms && special_file_mismatch (finfo, vers->vn_user, NULL)) + return 1; +#endif + + if (vers->entdata && vers->entdata->options) + options = xstrdup (vers->entdata->options); + else + options = xstrdup (""); + + tocvsPath = wrap_tocvs_process_file (finfo->file); + retcode = RCS_cmp_file (vers->srcfile, vers->vn_user, options, + tocvsPath == NULL ? finfo->file : tocvsPath); + if (retcode == 0) + { + /* no difference was found, so fix the entries file */ + ts = time_stamp (finfo->file); + Register (finfo->entries, finfo->file, + vers->vn_user ? vers->vn_user : vers->vn_rcs, ts, + options, vers->tag, vers->date, (char *) 0); +#ifdef SERVER_SUPPORT + if (server_active) + { + /* We need to update the entries line on the client side. */ + server_update_entries + (finfo->file, finfo->update_dir, finfo->repository, SERVER_UPDATED); + } +#endif + free (ts); + + /* update the entdata pointer in the vers_ts structure */ + p = findnode (finfo->entries, finfo->file); + vers->entdata = (Entnode *) p->data; + + ret = 0; + } + else + ret = 1; /* files were really different */ + + if (tocvsPath) + { + /* Need to call unlink myself because the noexec variable + * has been set to 1. */ + if (trace) + (void) fprintf (stderr, "%s-> unlink (%s)\n", + CLIENT_SERVER_STR, tocvsPath); + if ( CVS_UNLINK (tocvsPath) < 0) + error (0, errno, "could not remove %s", tocvsPath); + } + + free (options); + return (ret); +} diff --git a/gnu/dist/cvs/src/parseinfo.c b/gnu/dist/cvs/src/parseinfo.c new file mode 100644 index 00000000000..1c1ab70c87e --- /dev/null +++ b/gnu/dist/cvs/src/parseinfo.c @@ -0,0 +1,409 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + */ + +#include "cvs.h" +#include "getline.h" +#include <assert.h> + +/* + * Parse the INFOFILE file for the specified REPOSITORY. Invoke CALLPROC for + * the first line in the file that matches the REPOSITORY, or if ALL != 0, any lines + * matching "ALL", or if no lines match, the last line matching "DEFAULT". + * + * Return 0 for success, -1 if there was not an INFOFILE, and >0 for failure. + */ +int +Parse_Info (infofile, repository, callproc, all) + char *infofile; + char *repository; + CALLPROC callproc; + int all; +{ + int err = 0; + FILE *fp_info; + char *infopath; + char *line = NULL; + size_t line_allocated = 0; + char *default_value = NULL; + char *expanded_value= NULL; + int callback_done, line_number; + char *cp, *exp, *value, *srepos; + const char *regex_err; + + if (CVSroot_original == NULL) + { + /* XXX - should be error maybe? */ + error (0, 0, "CVSROOT variable not set"); + return (1); + } + + /* find the info file and open it */ + infopath = xmalloc (strlen (CVSroot_directory) + + strlen (infofile) + + sizeof (CVSROOTADM) + + 10); + (void) sprintf (infopath, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, infofile); + fp_info = CVS_FOPEN (infopath, "r"); + if (fp_info == NULL) + { + /* If no file, don't do anything special. */ + if (!existence_error (errno)) + error (0, errno, "cannot open %s", infopath); + free (infopath); + return 0; + } + + /* strip off the CVSROOT if repository was absolute */ + srepos = Short_Repository (repository); + + if (trace) + (void) fprintf (stderr, " -> ParseInfo(%s, %s, %s)\n", + infopath, srepos, all ? "ALL" : "not ALL"); + + /* search the info file for lines that match */ + callback_done = line_number = 0; + while (getline (&line, &line_allocated, fp_info) >= 0) + { + line_number++; + + /* skip lines starting with # */ + if (line[0] == '#') + continue; + + /* skip whitespace at beginning of line */ + for (cp = line; *cp && isspace ((unsigned char) *cp); cp++) + ; + + /* if *cp is null, the whole line was blank */ + if (*cp == '\0') + continue; + + /* the regular expression is everything up to the first space */ + for (exp = cp; *cp && !isspace ((unsigned char) *cp); cp++) + ; + if (*cp != '\0') + *cp++ = '\0'; + + /* skip whitespace up to the start of the matching value */ + while (*cp && isspace ((unsigned char) *cp)) + cp++; + + /* no value to match with the regular expression is an error */ + if (*cp == '\0') + { + error (0, 0, "syntax error at line %d file %s; ignored", + line_number, infofile); + continue; + } + value = cp; + + /* strip the newline off the end of the value */ + if ((cp = strrchr (value, '\n')) != NULL) + *cp = '\0'; + + if (expanded_value != NULL) + free (expanded_value); + expanded_value = expand_path (value, infofile, line_number); + if (!expanded_value) + { + continue; + } + + /* + * At this point, exp points to the regular expression, and value + * points to the value to call the callback routine with. Evaluate + * the regular expression against srepos and callback with the value + * if it matches. + */ + + /* save the default value so we have it later if we need it */ + if (strcmp (exp, "DEFAULT") == 0) + { + /* Is it OK to silently ignore all but the last DEFAULT + expression? */ + if (default_value != NULL) + free (default_value); + default_value = xstrdup (expanded_value); + continue; + } + + /* + * For a regular expression of "ALL", do the callback always We may + * execute lots of ALL callbacks in addition to *one* regular matching + * callback or default + */ + if (strcmp (exp, "ALL") == 0) + { + if (all) + err += callproc (repository, expanded_value); + else + error(0, 0, "Keyword `ALL' is ignored at line %d in %s file", + line_number, infofile); + continue; + } + + if (callback_done) + /* only first matching, plus "ALL"'s */ + continue; + + /* see if the repository matched this regular expression */ + if ((regex_err = re_comp (exp)) != NULL) + { + error (0, 0, "bad regular expression at line %d file %s: %s", + line_number, infofile, regex_err); + continue; + } + if (re_exec (srepos) == 0) + continue; /* no match */ + + /* it did, so do the callback and note that we did one */ + err += callproc (repository, expanded_value); + callback_done = 1; + } + if (ferror (fp_info)) + error (0, errno, "cannot read %s", infopath); + if (fclose (fp_info) < 0) + error (0, errno, "cannot close %s", infopath); + + /* if we fell through and didn't callback at all, do the default */ + if (callback_done == 0 && default_value != NULL) + err += callproc (repository, default_value); + + /* free up space if necessary */ + if (default_value != NULL) + free (default_value); + if (expanded_value != NULL) + free (expanded_value); + free (infopath); + if (line != NULL) + free (line); + + return (err); +} + + +/* Parse the CVS config file. The syntax right now is a bit ad hoc + but tries to draw on the best or more common features of the other + *info files and various unix (or non-unix) config file syntaxes. + Lines starting with # are comments. Settings are lines of the form + KEYWORD=VALUE. There is currently no way to have a multi-line + VALUE (would be nice if there was, probably). + + CVSROOT is the $CVSROOT directory (CVSroot_directory might not be + set yet). + + Returns 0 for success, negative value for failure. Call + error(0, ...) on errors in addition to the return value. */ +int +parse_config (cvsroot) + char *cvsroot; +{ + char *infopath; + FILE *fp_info; + char *line = NULL; + size_t line_allocated = 0; + size_t len; + char *p; + /* FIXME-reentrancy: If we do a multi-threaded server, this would need + to go to the per-connection data structures. */ + static int parsed = 0; + + /* Authentication code and serve_root might both want to call us. + Let this happen smoothly. */ + if (parsed) + return 0; + parsed = 1; + + infopath = malloc (strlen (cvsroot) + + sizeof (CVSROOTADM_CONFIG) + + sizeof (CVSROOTADM) + + 10); + if (infopath == NULL) + { + error (0, 0, "out of memory; cannot allocate infopath"); + goto error_return; + } + + strcpy (infopath, cvsroot); + strcat (infopath, "/"); + strcat (infopath, CVSROOTADM); + strcat (infopath, "/"); + strcat (infopath, CVSROOTADM_CONFIG); + + fp_info = CVS_FOPEN (infopath, "r"); + if (fp_info == NULL) + { + /* If no file, don't do anything special. */ + if (!existence_error (errno)) + { + /* Just a warning message; doesn't affect return + value, currently at least. */ + error (0, errno, "cannot open %s", infopath); + } + free (infopath); + return 0; + } + + while (getline (&line, &line_allocated, fp_info) >= 0) + { + /* Skip comments. */ + if (line[0] == '#') + continue; + + /* At least for the moment we don't skip whitespace at the start + of the line. Too picky? Maybe. But being insufficiently + picky leads to all sorts of confusion, and it is a lot easier + to start out picky and relax it than the other way around. + + Is there any kind of written standard for the syntax of this + sort of config file? Anywhere in POSIX for example (I guess + makefiles are sort of close)? Red Hat Linux has a bunch of + these too (with some GUI tools which edit them)... + + Along the same lines, we might want a table of keywords, + with various types (boolean, string, &c), as a mechanism + for making sure the syntax is consistent. Any good examples + to follow there (Apache?)? */ + + /* Strip the training newline. There will be one unless we + read a partial line without a newline, and then got end of + file (or error?). */ + + len = strlen (line) - 1; + if (line[len] == '\n') + line[len] = '\0'; + + /* Skip blank lines. */ + if (line[0] == '\0') + continue; + + /* The first '=' separates keyword from value. */ + p = strchr (line, '='); + if (p == NULL) + { + /* Probably should be printing line number. */ + error (0, 0, "syntax error in %s: line '%s' is missing '='", + infopath, line); + goto error_return; + } + + *p++ = '\0'; + + if (strcmp (line, "RCSBIN") == 0) + { + /* This option used to specify the directory for RCS + executables. But since we don't run them any more, + this is a noop. Silently ignore it so that a + repository can work with either new or old CVS. */ + ; + } + else if (strcmp (line, "SystemAuth") == 0) + { + if (strcmp (p, "no") == 0) +#ifdef AUTH_SERVER_SUPPORT + system_auth = 0; +#else + /* Still parse the syntax but ignore the + option. That way the same config file can + be used for local and server. */ + ; +#endif + else if (strcmp (p, "yes") == 0) +#ifdef AUTH_SERVER_SUPPORT + system_auth = 1; +#else + ; +#endif + else + { + error (0, 0, "unrecognized value '%s' for SystemAuth", p); + goto error_return; + } + } + else if (strcmp (line, "PreservePermissions") == 0) + { + if (strcmp (p, "no") == 0) + preserve_perms = 0; + else if (strcmp (p, "yes") == 0) + { +#ifdef PRESERVE_PERMISSIONS_SUPPORT + preserve_perms = 1; +#else + error (0, 0, "\ +warning: this CVS does not support PreservePermissions"); +#endif + } + else + { + error (0, 0, "unrecognized value '%s' for PreservePermissions", + p); + goto error_return; + } + } + else if (strcmp (line, "TopLevelAdmin") == 0) + { + if (strcmp (p, "no") == 0) + top_level_admin = 0; + else if (strcmp (p, "yes") == 0) + top_level_admin = 1; + else + { + error (0, 0, "unrecognized value '%s' for TopLevelAdmin", p); + goto error_return; + } + } + else if (strcmp (line, "LockDir") == 0) + { + if (lock_dir != NULL) + free (lock_dir); + lock_dir = xstrdup (p); + /* Could try some validity checking, like whether we can + opendir it or something, but I don't see any particular + reason to do that now rather than waiting until lock.c. */ + } + else + { + /* We may be dealing with a keyword which was added in a + subsequent version of CVS. In that case it is a good idea + to complain, as (1) the keyword might enable a behavior like + alternate locking behavior, in which it is dangerous and hard + to detect if some CVS's have it one way and others have it + the other way, (2) in general, having us not do what the user + had in mind when they put in the keyword violates the + principle of least surprise. Note that one corollary is + adding new keywords to your CVSROOT/config file is not + particularly recommended unless you are planning on using + the new features. */ + error (0, 0, "%s: unrecognized keyword '%s'", + infopath, line); + goto error_return; + } + } + if (ferror (fp_info)) + { + error (0, errno, "cannot read %s", infopath); + goto error_return; + } + if (fclose (fp_info) < 0) + { + error (0, errno, "cannot close %s", infopath); + goto error_return; + } + free (infopath); + if (line != NULL) + free (line); + return 0; + + error_return: + if (infopath != NULL) + free (infopath); + if (line != NULL) + free (line); + return -1; +} diff --git a/gnu/dist/cvs/src/patch.c b/gnu/dist/cvs/src/patch.c new file mode 100644 index 00000000000..f16a3318890 --- /dev/null +++ b/gnu/dist/cvs/src/patch.c @@ -0,0 +1,792 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Patch + * + * Create a Larry Wall format "patch" file between a previous release and the + * current head of a module, or between two releases. Can specify the + * release as either a date or a revision number. + */ + +#include "cvs.h" +#include "getline.h" + +static RETSIGTYPE patch_cleanup PROTO((void)); +static Dtype patch_dirproc PROTO ((void *callerdat, char *dir, + char *repos, char *update_dir, + List *entries)); +static int patch_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int patch_proc PROTO((int *pargc, char **argv, char *xwhere, + char *mwhere, char *mfile, int shorten, + int local_specified, char *mname, char *msg)); + +static int force_tag_match = 1; +static int patch_short = 0; +static int toptwo_diffs = 0; +static int local = 0; +static char *options = NULL; +static char *rev1 = NULL; +static int rev1_validated = 0; +static char *rev2 = NULL; +static int rev2_validated = 0; +static char *date1 = NULL; +static char *date2 = NULL; +static char *tmpfile1 = NULL; +static char *tmpfile2 = NULL; +static char *tmpfile3 = NULL; +static int unidiff = 0; + +static const char *const patch_usage[] = +{ + "Usage: %s %s [-flR] [-c|-u] [-s|-t] [-V %%d]\n", + " -r rev|-D date [-r rev2 | -D date2] modules...\n", + "\t-f\tForce a head revision match if tag/date not found.\n", + "\t-l\tLocal directory only, not recursive\n", + "\t-R\tProcess directories recursively.\n", + "\t-c\tContext diffs (default)\n", + "\t-u\tUnidiff format.\n", + "\t-s\tShort patch - one liner per file.\n", + "\t-t\tTop two diffs - last change made to the file.\n", + "\t-D date\tDate.\n", + "\t-r rev\tRevision - symbolic or numeric.\n", + "\t-V vers\tUse RCS Version \"vers\" for keyword expansion.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +patch (argc, argv) + int argc; + char **argv; +{ + register int i; + int c; + int err = 0; + DBM *db; + + if (argc == -1) + usage (patch_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+V:k:cuftsQqlRD:r:")) != -1) + { + switch (c) + { + case 'Q': + case 'q': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'f': + force_tag_match = 0; + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 't': + toptwo_diffs = 1; + break; + case 's': + patch_short = 1; + break; + case 'D': + if (rev2 != NULL || date2 != NULL) + error (1, 0, + "no more than two revisions/dates can be specified"); + if (rev1 != NULL || date1 != NULL) + date2 = Make_Date (optarg); + else + date1 = Make_Date (optarg); + break; + case 'r': + if (rev2 != NULL || date2 != NULL) + error (1, 0, + "no more than two revisions/dates can be specified"); + if (rev1 != NULL || date1 != NULL) + rev2 = optarg; + else + rev1 = optarg; + break; + case 'k': + if (options) + free (options); + options = RCS_check_kflag (optarg); + break; + case 'V': + /* This option is pretty seriously broken: + 1. It is not clear what it does (does it change keyword + expansion behavior? If so, how? Or does it have + something to do with what version of RCS we are using? + Or the format we write RCS files in?). + 2. Because both it and -k use the options variable, + specifying both -V and -k doesn't work. + 3. At least as of CVS 1.9, it doesn't work (failed + assertion in RCS_checkout where it asserts that options + starts with -k). Few people seem to be complaining. + In the future (perhaps the near future), I have in mind + removing it entirely, and updating NEWS and cvs.texinfo, + but in case it is a good idea to give people more time + to complain if they would miss it, I'll just add this + quick and dirty error message for now. */ + error (1, 0, + "the -V option is obsolete and should not be used"); +#if 0 + if (atoi (optarg) <= 0) + error (1, 0, "must specify a version number to -V"); + if (options) + free (options); + options = xmalloc (strlen (optarg) + 1 + 2); /* for the -V */ + (void) sprintf (options, "-V%s", optarg); +#endif + break; + case 'u': + unidiff = 1; /* Unidiff */ + break; + case 'c': /* Context diff */ + unidiff = 0; + break; + case '?': + default: + usage (patch_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* Sanity checks */ + if (argc < 1) + usage (patch_usage); + + if (toptwo_diffs && patch_short) + error (1, 0, "-t and -s options are mutually exclusive"); + if (toptwo_diffs && (date1 != NULL || date2 != NULL || + rev1 != NULL || rev2 != NULL)) + error (1, 0, "must not specify revisions/dates with -t option!"); + + if (!toptwo_diffs && (date1 == NULL && date2 == NULL && + rev1 == NULL && rev2 == NULL)) + error (1, 0, "must specify at least one revision/date!"); + if (date1 != NULL && date2 != NULL) + if (RCS_datecmp (date1, date2) >= 0) + error (1, 0, "second date must come after first date!"); + + /* if options is NULL, make it a NULL string */ + if (options == NULL) + options = xstrdup (""); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* We're the client side. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (local) + send_arg("-l"); + if (!force_tag_match) + send_arg("-f"); + if (toptwo_diffs) + send_arg("-t"); + if (patch_short) + send_arg("-s"); + if (unidiff) + send_arg("-u"); + + if (rev1) + option_with_arg ("-r", rev1); + if (date1) + client_senddate (date1); + if (rev2) + option_with_arg ("-r", rev2); + if (date2) + client_senddate (date2); + if (options[0] != '\0') + send_arg (options); + + { + int i; + for (i = 0; i < argc; ++i) + send_arg (argv[i]); + } + + send_to_server ("rdiff\012", 0); + return get_responses_and_close (); + } +#endif + + /* clean up if we get a signal */ +#ifdef SIGHUP + (void) SIG_register (SIGHUP, patch_cleanup); +#endif +#ifdef SIGINT + (void) SIG_register (SIGINT, patch_cleanup); +#endif +#ifdef SIGQUIT + (void) SIG_register (SIGQUIT, patch_cleanup); +#endif +#ifdef SIGPIPE + (void) SIG_register (SIGPIPE, patch_cleanup); +#endif +#ifdef SIGTERM + (void) SIG_register (SIGTERM, patch_cleanup); +#endif + + db = open_module (); + for (i = 0; i < argc; i++) + err += do_module (db, argv[i], PATCH, "Patching", patch_proc, + (char *) NULL, 0, 0, 0, (char *) NULL); + close_module (db); + free (options); + patch_cleanup (); + return (err); +} + +/* + * callback proc for doing the real work of patching + */ +/* ARGSUSED */ +static int +patch_proc (pargc, argv, xwhere, mwhere, mfile, shorten, local_specified, + mname, msg) + int *pargc; + char **argv; + char *xwhere; + char *mwhere; + char *mfile; + int shorten; + int local_specified; + char *mname; + char *msg; +{ + int err = 0; + int which; + char *repository; + char *where; + + repository = xmalloc (strlen (CVSroot_directory) + strlen (argv[0]) + + (mfile == NULL ? 0 : strlen (mfile)) + 30); + (void) sprintf (repository, "%s/%s", CVSroot_directory, argv[0]); + where = xmalloc (strlen (argv[0]) + (mfile == NULL ? 0 : strlen (mfile)) + + 10); + (void) strcpy (where, argv[0]); + + /* if mfile isn't null, we need to set up to do only part of the module */ + if (mfile != NULL) + { + char *cp; + char *path; + + /* if the portion of the module is a path, put the dir part on repos */ + if ((cp = strrchr (mfile, '/')) != NULL) + { + *cp = '\0'; + (void) strcat (repository, "/"); + (void) strcat (repository, mfile); + (void) strcat (where, "/"); + (void) strcat (where, mfile); + mfile = cp + 1; + } + + /* take care of the rest */ + path = xmalloc (strlen (repository) + strlen (mfile) + 5); + (void) sprintf (path, "%s/%s", repository, mfile); + if (isdir (path)) + { + /* directory means repository gets the dir tacked on */ + (void) strcpy (repository, path); + (void) strcat (where, "/"); + (void) strcat (where, mfile); + } + else + { + int i; + + /* a file means muck argv */ + for (i = 1; i < *pargc; i++) + free (argv[i]); + argv[1] = xstrdup (mfile); + (*pargc) = 2; + } + free (path); + } + + /* cd to the starting repository */ + if ( CVS_CHDIR (repository) < 0) + { + error (0, errno, "cannot chdir to %s", repository); + free (repository); + return (1); + } + free (repository); + + if (force_tag_match) + which = W_REPOS | W_ATTIC; + else + which = W_REPOS; + + if (rev1 != NULL && !rev1_validated) + { + tag_check_valid (rev1, *pargc - 1, argv + 1, local, 0, NULL); + rev1_validated = 1; + } + if (rev2 != NULL && !rev2_validated) + { + tag_check_valid (rev2, *pargc - 1, argv + 1, local, 0, NULL); + rev2_validated = 1; + } + + /* start the recursion processor */ + err = start_recursion (patch_fileproc, (FILESDONEPROC) NULL, patch_dirproc, + (DIRLEAVEPROC) NULL, NULL, + *pargc - 1, argv + 1, local, + which, 0, 1, where, 1); + free (where); + + return (err); +} + +/* + * Called to examine a particular RCS file, as appropriate with the options + * that were set above. + */ +/* ARGSUSED */ +static int +patch_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + struct utimbuf t; + char *vers_tag, *vers_head; + char *rcs = NULL; + RCSNode *rcsfile; + FILE *fp1, *fp2, *fp3; + int ret = 0; + int isattic = 0; + int retcode = 0; + char *file1; + char *file2; + char *strippath; + char *line1, *line2; + size_t line1_chars_allocated; + size_t line2_chars_allocated; + char *cp1, *cp2; + FILE *fp; + int line_length; + + line1 = NULL; + line1_chars_allocated = 0; + line2 = NULL; + line2_chars_allocated = 0; + + /* find the parsed rcs file */ + if ((rcsfile = finfo->rcs) == NULL) + { + ret = 1; + goto out2; + } + if ((rcsfile->flags & VALID) && (rcsfile->flags & INATTIC)) + isattic = 1; + + rcs = xmalloc (strlen (finfo->file) + sizeof (RCSEXT) + 5); + (void) sprintf (rcs, "%s%s", finfo->file, RCSEXT); + + /* if vers_head is NULL, may have been removed from the release */ + if (isattic && rev2 == NULL && date2 == NULL) + vers_head = NULL; + else + { + vers_head = RCS_getversion (rcsfile, rev2, date2, force_tag_match, + (int *) NULL); + if (vers_head != NULL && RCS_isdead (rcsfile, vers_head)) + { + free (vers_head); + vers_head = NULL; + } + } + + if (toptwo_diffs) + { + if (vers_head == NULL) + { + ret = 1; + goto out2; + } + + if (!date1) + date1 = xmalloc (MAXDATELEN); + *date1 = '\0'; + if (RCS_getrevtime (rcsfile, vers_head, date1, 1) == -1) + { + if (!really_quiet) + error (0, 0, "cannot find date in rcs file %s revision %s", + rcs, vers_head); + ret = 1; + goto out2; + } + } + vers_tag = RCS_getversion (rcsfile, rev1, date1, force_tag_match, + (int *) NULL); + if (vers_tag != NULL && RCS_isdead (rcsfile, vers_tag)) + { + free (vers_tag); + vers_tag = NULL; + } + + if (vers_tag == NULL && vers_head == NULL) + { + /* Nothing known about specified revs. */ + ret = 0; + goto out2; + } + + if (vers_tag && vers_head && strcmp (vers_head, vers_tag) == 0) + { + /* Not changed between releases. */ + ret = 0; + goto out2; + } + + if (patch_short) + { + cvs_output ("File ", 0); + cvs_output (finfo->fullname, 0); + if (vers_tag == NULL) + { + cvs_output (" is new; current revision ", 0); + cvs_output (vers_head, 0); + cvs_output ("\n", 1); + } + else if (vers_head == NULL) + { + cvs_output (" is removed; not included in ", 0); + if (rev2 != NULL) + { + cvs_output ("release tag ", 0); + cvs_output (rev2, 0); + } + else if (date2 != NULL) + { + cvs_output ("release date ", 0); + cvs_output (date2, 0); + } + else + cvs_output ("current release", 0); + cvs_output ("\n", 1); + } + else + { + cvs_output (" changed from revision ", 0); + cvs_output (vers_tag, 0); + cvs_output (" to ", 0); + cvs_output (vers_head, 0); + cvs_output ("\n", 1); + } + ret = 0; + goto out2; + } + + /* Create 3 empty files. I'm not really sure there is any advantage + to doing so now rather than just waiting until later. */ + tmpfile1 = cvs_temp_name (); + fp1 = CVS_FOPEN (tmpfile1, "w+"); + if (fp1 == NULL) + { + error (0, errno, "cannot create temporary file %s", tmpfile1); + ret = 1; + goto out; + } + else + if (fclose (fp1) < 0) + error (0, errno, "warning: cannot close %s", tmpfile1); + tmpfile2 = cvs_temp_name (); + fp2 = CVS_FOPEN (tmpfile2, "w+"); + if (fp2 == NULL) + { + error (0, errno, "cannot create temporary file %s", tmpfile2); + ret = 1; + goto out; + } + else + if (fclose (fp2) < 0) + error (0, errno, "warning: cannot close %s", tmpfile2); + tmpfile3 = cvs_temp_name (); + fp3 = CVS_FOPEN (tmpfile3, "w+"); + if (fp3 == NULL) + { + error (0, errno, "cannot create temporary file %s", tmpfile3); + ret = 1; + goto out; + } + else + if (fclose (fp3) < 0) + error (0, errno, "warning: cannot close %s", tmpfile3); + + if (vers_tag != NULL) + { + retcode = RCS_checkout (rcsfile, (char *) NULL, vers_tag, + rev1, options, tmpfile1, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + if (retcode != 0) + { + error (0, 0, + "cannot check out revision %s of %s", vers_tag, rcs); + ret = 1; + goto out; + } + memset ((char *) &t, 0, sizeof (t)); + if ((t.actime = t.modtime = RCS_getrevtime (rcsfile, vers_tag, + (char *) 0, 0)) != -1) + /* I believe this timestamp only affects the dates in our diffs, + and therefore should be on the server, not the client. */ + (void) utime (tmpfile1, &t); + } + else if (toptwo_diffs) + { + ret = 1; + goto out; + } + if (vers_head != NULL) + { + retcode = RCS_checkout (rcsfile, (char *) NULL, vers_head, + rev2, options, tmpfile2, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + if (retcode != 0) + { + error (0, 0, + "cannot check out revision %s of %s", vers_head, rcs); + ret = 1; + goto out; + } + if ((t.actime = t.modtime = RCS_getrevtime (rcsfile, vers_head, + (char *) 0, 0)) != -1) + /* I believe this timestamp only affects the dates in our diffs, + and therefore should be on the server, not the client. */ + (void) utime (tmpfile2, &t); + } + + switch (diff_exec (tmpfile1, tmpfile2, unidiff ? "-u" : "-c", tmpfile3)) + { + case -1: /* fork/wait failure */ + error (1, errno, "fork for diff failed on %s", rcs); + break; + case 0: /* nothing to do */ + break; + case 1: + /* + * The two revisions are really different, so read the first two + * lines of the diff output file, and munge them to include more + * reasonable file names that "patch" will understand. + */ + + /* Output an "Index:" line for patch to use */ + cvs_output ("Index: ", 0); + cvs_output (finfo->fullname, 0); + cvs_output ("\n", 1); + + fp = open_file (tmpfile3, "r"); + if (getline (&line1, &line1_chars_allocated, fp) < 0 || + getline (&line2, &line2_chars_allocated, fp) < 0) + { + if (feof (fp)) + error (0, 0, "\ +failed to read diff file header %s for %s: end of file", tmpfile3, rcs); + else + error (0, errno, + "failed to read diff file header %s for %s", + tmpfile3, rcs); + ret = 1; + if (fclose (fp) < 0) + error (0, errno, "error closing %s", tmpfile3); + goto out; + } + if (!unidiff) + { + if (strncmp (line1, "*** ", 4) != 0 || + strncmp (line2, "--- ", 4) != 0 || + (cp1 = strchr (line1, '\t')) == NULL || + (cp2 = strchr (line2, '\t')) == NULL) + { + error (0, 0, "invalid diff header for %s", rcs); + ret = 1; + if (fclose (fp) < 0) + error (0, errno, "error closing %s", tmpfile3); + goto out; + } + } + else + { + if (strncmp (line1, "--- ", 4) != 0 || + strncmp (line2, "+++ ", 4) != 0 || + (cp1 = strchr (line1, '\t')) == NULL || + (cp2 = strchr (line2, '\t')) == NULL) + { + error (0, 0, "invalid unidiff header for %s", rcs); + ret = 1; + if (fclose (fp) < 0) + error (0, errno, "error closing %s", tmpfile3); + goto out; + } + } + if (CVSroot_directory != NULL) + { + strippath = xmalloc (strlen (CVSroot_directory) + 10); + (void) sprintf (strippath, "%s/", CVSroot_directory); + } + else + strippath = xstrdup (REPOS_STRIP); + if (strncmp (rcs, strippath, strlen (strippath)) == 0) + rcs += strlen (strippath); + free (strippath); + if (vers_tag != NULL) + { + file1 = xmalloc (strlen (finfo->fullname) + + strlen (vers_tag) + + 10); + (void) sprintf (file1, "%s:%s", finfo->fullname, vers_tag); + } + else + { + file1 = xstrdup (DEVNULL); + } + file2 = xmalloc (strlen (finfo->fullname) + + (vers_head != NULL ? strlen (vers_head) : 10) + + 10); + (void) sprintf (file2, "%s:%s", finfo->fullname, + vers_head ? vers_head : "removed"); + + /* Note that the string "diff" is specified by POSIX (for -c) + and is part of the diff output format, not the name of a + program. */ + if (unidiff) + { + cvs_output ("diff -u ", 0); + cvs_output (file1, 0); + cvs_output (" ", 1); + cvs_output (file2, 0); + cvs_output ("\n", 1); + + cvs_output ("--- ", 0); + cvs_output (file1, 0); + cvs_output (cp1, 0); + cvs_output ("+++ ", 0); + } + else + { + cvs_output ("diff -c ", 0); + cvs_output (file1, 0); + cvs_output (" ", 1); + cvs_output (file2, 0); + cvs_output ("\n", 1); + + cvs_output ("*** ", 0); + cvs_output (file1, 0); + cvs_output (cp1, 0); + cvs_output ("--- ", 0); + } + + cvs_output (finfo->fullname, 0); + cvs_output (cp2, 0); + + /* spew the rest of the diff out */ + while ((line_length + = getline (&line1, &line1_chars_allocated, fp)) + >= 0) + cvs_output (line1, 0); + if (line_length < 0 && !feof (fp)) + error (0, errno, "cannot read %s", tmpfile3); + + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", tmpfile3); + free (file1); + free (file2); + break; + default: + error (0, 0, "diff failed for %s", finfo->fullname); + } + out: + if (line1) + free (line1); + if (line2) + free (line2); + if (CVS_UNLINK (tmpfile1) < 0) + error (0, errno, "cannot unlink %s", tmpfile1); + if (CVS_UNLINK (tmpfile2) < 0) + error (0, errno, "cannot unlink %s", tmpfile2); + if (CVS_UNLINK (tmpfile3) < 0) + error (0, errno, "cannot unlink %s", tmpfile3); + free (tmpfile1); + free (tmpfile2); + free (tmpfile3); + tmpfile1 = tmpfile2 = tmpfile3 = NULL; + + out2: + if (rcs != NULL) + free (rcs); + return (ret); +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +patch_dirproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + char *dir; + char *repos; + char *update_dir; + List *entries; +{ + if (!quiet) + error (0, 0, "Diffing %s", update_dir); + return (R_PROCESS); +} + +/* + * Clean up temporary files + */ +static RETSIGTYPE +patch_cleanup () +{ + /* Note that the checks for existence_error are because we are + called from a signal handler, without SIG_begincrsect, so + we don't know whether the files got created. */ + + if (tmpfile1 != NULL) + { + if (unlink_file (tmpfile1) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", tmpfile1); + free (tmpfile1); + } + if (tmpfile2 != NULL) + { + if (unlink_file (tmpfile2) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", tmpfile2); + free (tmpfile2); + } + if (tmpfile3 != NULL) + { + if (unlink_file (tmpfile3) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", tmpfile3); + free (tmpfile3); + } + tmpfile1 = tmpfile2 = tmpfile3 = NULL; +} diff --git a/gnu/dist/cvs/src/rcs.c b/gnu/dist/cvs/src/rcs.c new file mode 100644 index 00000000000..3af905b0c66 --- /dev/null +++ b/gnu/dist/cvs/src/rcs.c @@ -0,0 +1,8926 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * The routines contained in this file do all the rcs file parsing and + * manipulation + */ + +#include <assert.h> +#include "cvs.h" +#include "edit.h" +#include "hardlink.h" + +int preserve_perms = 0; + +/* The RCS -k options, and a set of enums that must match the array. + These come first so that we can use enum kflag in function + prototypes. */ +static const char *const kflags[] = + {"kv", "kvl", "k", "v", "o", "b", (char *) NULL}; +enum kflag { KFLAG_KV = 0, KFLAG_KVL, KFLAG_K, KFLAG_V, KFLAG_O, KFLAG_B }; + +/* A structure we use to buffer the contents of an RCS file. The + various fields are only referenced directly by the rcsbuf_* + functions. We declare the struct here so that we can allocate it + on the stack, rather than in memory. */ + +struct rcsbuffer +{ + /* Points to the current position in the buffer. */ + char *ptr; + /* Points just after the last valid character in the buffer. */ + char *ptrend; + /* The file. */ + FILE *fp; + /* The name of the file, used for error messages. */ + const char *filename; + /* The starting file position of the data in the buffer. */ + unsigned long pos; + /* The length of the value. */ + size_t vlen; + /* Whether the value contains an '@' string. If so, we can not + compress whitespace characters. */ + int at_string; + /* The number of embedded '@' characters in an '@' string. If + this is non-zero, we must search the string for pairs of '@' + and convert them to a single '@'. */ + int embedded_at; +}; + +static RCSNode *RCS_parsercsfile_i PROTO((FILE * fp, const char *rcsfile)); +static char *RCS_getdatebranch PROTO((RCSNode * rcs, char *date, char *branch)); +static void rcsbuf_open PROTO ((struct rcsbuffer *, FILE *fp, + const char *filename, unsigned long pos)); +static void rcsbuf_close PROTO ((struct rcsbuffer *)); +static int rcsbuf_getkey PROTO ((struct rcsbuffer *, char **keyp, + char **valp)); +static int rcsbuf_getrevnum PROTO ((struct rcsbuffer *, char **revp)); +static char *rcsbuf_fill PROTO ((struct rcsbuffer *, char *ptr, char **keyp, + char **valp)); +static char *rcsbuf_valcopy PROTO ((struct rcsbuffer *, char *val, int polish, + size_t *lenp)); +static void rcsbuf_valpolish PROTO ((struct rcsbuffer *, char *val, int polish, + size_t *lenp)); +static void rcsbuf_valpolish_internal PROTO ((struct rcsbuffer *, char *to, + const char *from, size_t *lenp)); +static unsigned long rcsbuf_ftell PROTO ((struct rcsbuffer *)); +static void rcsbuf_get_buffered PROTO ((struct rcsbuffer *, char **datap, + size_t *lenp)); +static void rcsbuf_cache PROTO ((RCSNode *, struct rcsbuffer *)); +static void rcsbuf_cache_close PROTO ((void)); +static void rcsbuf_cache_open PROTO ((RCSNode *, long, FILE **, + struct rcsbuffer *)); +static int checkmagic_proc PROTO((Node *p, void *closure)); +static void do_branches PROTO((List * list, char *val)); +static void do_symbols PROTO((List * list, char *val)); +static void do_locks PROTO((List * list, char *val)); +static void free_rcsnode_contents PROTO((RCSNode *)); +static void free_rcsvers_contents PROTO((RCSVers *)); +static void rcsvers_delproc PROTO((Node * p)); +static char *translate_symtag PROTO((RCSNode *, const char *)); +static char *RCS_addbranch PROTO ((RCSNode *, const char *)); +static char *truncate_revnum_in_place PROTO ((char *)); +static char *truncate_revnum PROTO ((const char *)); +static char *printable_date PROTO((const char *)); +static char *escape_keyword_value PROTO ((const char *, int *)); +static void expand_keywords PROTO((RCSNode *, RCSVers *, const char *, + const char *, size_t, enum kflag, char *, + size_t, char **, size_t *)); +static void cmp_file_buffer PROTO((void *, const char *, size_t)); + +enum rcs_delta_op {RCS_ANNOTATE, RCS_FETCH}; +static void RCS_deltas PROTO ((RCSNode *, FILE *, struct rcsbuffer *, char *, + enum rcs_delta_op, char **, size_t *, + char **, size_t *)); + +/* Routines for reading, parsing and writing RCS files. */ +static RCSVers *getdelta PROTO ((struct rcsbuffer *, char *, char **, + char **)); +static Deltatext *RCS_getdeltatext PROTO ((RCSNode *, FILE *, + struct rcsbuffer *)); +static void freedeltatext PROTO ((Deltatext *)); + +static void RCS_putadmin PROTO ((RCSNode *, FILE *)); +static void RCS_putdtree PROTO ((RCSNode *, char *, FILE *)); +static void RCS_putdesc PROTO ((RCSNode *, FILE *)); +static void putdelta PROTO ((RCSVers *, FILE *)); +static int putrcsfield_proc PROTO ((Node *, void *)); +static int putsymbol_proc PROTO ((Node *, void *)); +static void RCS_copydeltas PROTO ((RCSNode *, FILE *, struct rcsbuffer *, + FILE *, Deltatext *, char *)); +static int count_delta_actions PROTO ((Node *, void *)); +static void putdeltatext PROTO ((FILE *, Deltatext *)); + +static FILE *rcs_internal_lockfile PROTO ((char *)); +static void rcs_internal_unlockfile PROTO ((FILE *, char *)); +static char *rcs_lockfilename PROTO ((char *)); + +/* The RCS file reading functions are called a lot, and they do some + string comparisons. This macro speeds things up a bit by skipping + the function call when the first characters are different. It + evaluates its arguments multiple times. */ +#define STREQ(a, b) ((a)[0] == (b)[0] && strcmp ((a), (b)) == 0) + +/* + * We don't want to use isspace() from the C library because: + * + * 1. The definition of "whitespace" in RCS files includes ASCII + * backspace, but the C locale doesn't. + * 2. isspace is an very expensive function call in some implementations + * due to the addition of wide character support. + */ +static const char spacetab[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, /* 0x00 - 0x0f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x10 - 0x1f */ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x20 - 0x2f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x30 - 0x3f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x40 - 0x4f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x50 - 0x5f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x60 - 0x8f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x70 - 0x7f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x80 - 0x8f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x90 - 0x9f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xa0 - 0xaf */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xb0 - 0xbf */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xc0 - 0xcf */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xd0 - 0xdf */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xe0 - 0xef */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* 0xf0 - 0xff */ +}; + +#define whitespace(c) (spacetab[(unsigned char)c] != 0) + +static char *rcs_lockfile; + +/* A few generic thoughts on error handling, in particular the + printing of unexpected characters that we find in the RCS file + (that is, why we use '\x%x' rather than %c or some such). + + * Avoiding %c means we don't have to worry about what is printable + and other such stuff. In error handling, often better to keep it + simple. + + * Hex rather than decimal or octal because character set standards + tend to use hex. + + * Saying "character 0x%x" might make it sound like we are printing + a file offset. So we use '\x%x'. + + * Would be nice to print the offset within the file, but I can + imagine various portability hassles (in particular, whether + unsigned long is always big enough to hold file offsets). */ + +/* Parse an rcsfile given a user file name and a repository. If there is + an error, we print an error message and return NULL. If the file + does not exist, we return NULL without printing anything (I'm not + sure this allows the caller to do anything reasonable, but it is + the current behavior). */ +RCSNode * +RCS_parse (file, repos) + const char *file; + const char *repos; +{ + RCSNode *rcs; + FILE *fp; + RCSNode *retval; + char *rcsfile; + + /* We're creating a new RCSNode, so there is no hope of finding it + in the cache. */ + rcsbuf_cache_close (); + + rcsfile = xmalloc (strlen (repos) + strlen (file) + + sizeof (RCSEXT) + sizeof (CVSATTIC) + 10); + (void) sprintf (rcsfile, "%s/%s%s", repos, file, RCSEXT); + if ((fp = CVS_FOPEN (rcsfile, FOPEN_BINARY_READ)) != NULL) + { + rcs = RCS_parsercsfile_i(fp, rcsfile); + if (rcs != NULL) + rcs->flags |= VALID; + + retval = rcs; + goto out; + } + else if (! existence_error (errno)) + { + error (0, errno, "cannot open %s", rcsfile); + retval = NULL; + goto out; + } + + (void) sprintf (rcsfile, "%s/%s/%s%s", repos, CVSATTIC, file, RCSEXT); + if ((fp = CVS_FOPEN (rcsfile, FOPEN_BINARY_READ)) != NULL) + { + rcs = RCS_parsercsfile_i(fp, rcsfile); + if (rcs != NULL) + { + rcs->flags |= INATTIC; + rcs->flags |= VALID; + } + + retval = rcs; + goto out; + } + else if (! existence_error (errno)) + { + error (0, errno, "cannot open %s", rcsfile); + retval = NULL; + goto out; + } +#if defined (SERVER_SUPPORT) && !defined (FILENAMES_CASE_INSENSITIVE) + else if (ign_case) + { + int status; + char *found_path; + + /* The client might be asking for a file which we do have + (which the client doesn't know about), but for which the + filename case differs. We only consider this case if the + regular CVS_FOPENs fail, because fopen_case is such an + expensive call. */ + (void) sprintf (rcsfile, "%s/%s%s", repos, file, RCSEXT); + status = fopen_case (rcsfile, "rb", &fp, &found_path); + if (status == 0) + { + rcs = RCS_parsercsfile_i (fp, rcsfile); + if (rcs != NULL) + rcs->flags |= VALID; + + free (rcs->path); + rcs->path = found_path; + retval = rcs; + goto out; + } + else if (! existence_error (status)) + { + error (0, status, "cannot open %s", rcsfile); + retval = NULL; + goto out; + } + + (void) sprintf (rcsfile, "%s/%s/%s%s", repos, CVSATTIC, file, RCSEXT); + status = fopen_case (rcsfile, "rb", &fp, &found_path); + if (status == 0) + { + rcs = RCS_parsercsfile_i (fp, rcsfile); + if (rcs != NULL) + { + rcs->flags |= INATTIC; + rcs->flags |= VALID; + } + + free (rcs->path); + rcs->path = found_path; + retval = rcs; + goto out; + } + else if (! existence_error (status)) + { + error (0, status, "cannot open %s", rcsfile); + retval = NULL; + goto out; + } + } +#endif + retval = NULL; + + out: + free (rcsfile); + + return retval; +} + +/* + * Parse a specific rcsfile. + */ +RCSNode * +RCS_parsercsfile (rcsfile) + char *rcsfile; +{ + FILE *fp; + RCSNode *rcs; + + /* We're creating a new RCSNode, so there is no hope of finding it + in the cache. */ + rcsbuf_cache_close (); + + /* open the rcsfile */ + if ((fp = CVS_FOPEN (rcsfile, FOPEN_BINARY_READ)) == NULL) + { + error (0, errno, "Couldn't open rcs file `%s'", rcsfile); + return (NULL); + } + + rcs = RCS_parsercsfile_i (fp, rcsfile); + + return (rcs); +} + + +/* + */ +static RCSNode * +RCS_parsercsfile_i (fp, rcsfile) + FILE *fp; + const char *rcsfile; +{ + RCSNode *rdata; + struct rcsbuffer rcsbuf; + char *key, *value; + + /* make a node */ + rdata = (RCSNode *) xmalloc (sizeof (RCSNode)); + memset ((char *) rdata, 0, sizeof (RCSNode)); + rdata->refcount = 1; + rdata->path = xstrdup (rcsfile); + + /* Process HEAD, BRANCH, and EXPAND keywords from the RCS header. + + Most cvs operations on the main branch don't need any more + information. Those that do call RCS_reparsercsfile to parse + the rest of the header and the deltas. */ + + rcsbuf_open (&rcsbuf, fp, rcsfile, 0); + + if (! rcsbuf_getkey (&rcsbuf, &key, &value)) + goto l_error; + if (STREQ (key, RCSDESC)) + goto l_error; + + if (STREQ (RCSHEAD, key) && value != NULL) + rdata->head = rcsbuf_valcopy (&rcsbuf, value, 0, (size_t *) NULL); + + if (! rcsbuf_getkey (&rcsbuf, &key, &value)) + goto l_error; + if (STREQ (key, RCSDESC)) + goto l_error; + + if (STREQ (RCSBRANCH, key) && value != NULL) + { + char *cp; + + rdata->branch = rcsbuf_valcopy (&rcsbuf, value, 0, (size_t *) NULL); + if ((numdots (rdata->branch) & 1) != 0) + { + /* turn it into a branch if it's a revision */ + cp = strrchr (rdata->branch, '.'); + *cp = '\0'; + } + } + + /* Look ahead for expand, stopping when we see desc or a revision + number. */ + while (1) + { + char *cp; + + if (STREQ (RCSEXPAND, key)) + { + rdata->expand = rcsbuf_valcopy (&rcsbuf, value, 0, + (size_t *) NULL); + break; + } + + for (cp = key; + (isdigit ((unsigned char) *cp) || *cp == '.') && *cp != '\0'; + cp++) + /* do nothing */ ; + if (*cp == '\0') + break; + + if (STREQ (RCSDESC, key)) + break; + + if (! rcsbuf_getkey (&rcsbuf, &key, &value)) + break; + } + + rdata->flags |= PARTIAL; + + rcsbuf_cache (rdata, &rcsbuf); + + return rdata; + +l_error: + error (0, 0, "`%s' does not appear to be a valid rcs file", + rcsfile); + rcsbuf_close (&rcsbuf); + freercsnode (&rdata); + fclose (fp); + return (NULL); +} + + +/* Do the real work of parsing an RCS file. + + On error, die with a fatal error; if it returns at all it was successful. + + If PFP is NULL, close the file when done. Otherwise, leave it open + and store the FILE * in *PFP. */ +void +RCS_reparsercsfile (rdata, pfp, rcsbufp) + RCSNode *rdata; + FILE **pfp; + struct rcsbuffer *rcsbufp; +{ + FILE *fp; + char *rcsfile; + struct rcsbuffer rcsbuf; + Node *q, *kv; + RCSVers *vnode; + int gotkey; + char *cp; + char *key, *value; + + assert (rdata != NULL); + rcsfile = rdata->path; + + rcsbuf_cache_open (rdata, 0, &fp, &rcsbuf); + + /* make a node */ + /* This probably shouldn't be done until later: if a file has an + empty revision tree (which is permissible), rdata->versions + should be NULL. -twp */ + rdata->versions = getlist (); + + /* + * process all the special header information, break out when we get to + * the first revision delta + */ + gotkey = 0; + for (;;) + { + /* get the next key/value pair */ + if (!gotkey) + { + if (! rcsbuf_getkey (&rcsbuf, &key, &value)) + { + error (1, 0, "`%s' does not appear to be a valid rcs file", + rcsfile); + } + } + + gotkey = 0; + + /* Skip head, branch and expand tags; we already have them. */ + if (STREQ (key, RCSHEAD) + || STREQ (key, RCSBRANCH) + || STREQ (key, RCSEXPAND)) + { + continue; + } + + if (STREQ (key, "access")) + { + if (value != NULL) + { + /* We pass the POLISH parameter as 1 because + RCS_addaccess expects nothing but spaces. FIXME: + It would be easy and more efficient to change + RCS_addaccess. */ + rdata->access = rcsbuf_valcopy (&rcsbuf, value, 1, + (size_t *) NULL); + } + continue; + } + + /* We always save lock information, so that we can handle + -kkvl correctly when checking out a file. */ + if (STREQ (key, "locks")) + { + if (value != NULL) + rdata->locks_data = rcsbuf_valcopy (&rcsbuf, value, 0, + (size_t *) NULL); + if (! rcsbuf_getkey (&rcsbuf, &key, &value)) + { + error (1, 0, "premature end of file reading %s", rcsfile); + } + if (STREQ (key, "strict") && value == NULL) + { + rdata->strict_locks = 1; + } + else + gotkey = 1; + continue; + } + + if (STREQ (RCSSYMBOLS, key)) + { + if (value != NULL) + rdata->symbols_data = rcsbuf_valcopy (&rcsbuf, value, 0, + (size_t *) NULL); + continue; + } + + /* + * check key for '.''s and digits (probably a rev) if it is a + * revision or `desc', we are done with the headers and are down to the + * revision deltas, so we break out of the loop + */ + for (cp = key; + (isdigit ((unsigned char) *cp) || *cp == '.') && *cp != '\0'; + cp++) + /* do nothing */ ; + /* Note that when comparing with RCSDATE, we are not massaging + VALUE from the string found in the RCS file. This is OK + since we know exactly what to expect. */ + if (*cp == '\0' && strncmp (RCSDATE, value, (sizeof RCSDATE) - 1) == 0) + break; + + if (STREQ (key, RCSDESC)) + break; + + if (STREQ (key, "comment")) + { + rdata->comment = rcsbuf_valcopy (&rcsbuf, value, 0, + (size_t *) NULL); + continue; + } + if (rdata->other == NULL) + rdata->other = getlist (); + kv = getnode (); + kv->type = RCSFIELD; + kv->key = xstrdup (key); + kv->data = rcsbuf_valcopy (&rcsbuf, value, 1, (size_t *) NULL); + if (addnode (rdata->other, kv) != 0) + { + error (0, 0, "warning: duplicate key `%s' in RCS file `%s'", + key, rcsfile); + freenode (kv); + } + + /* if we haven't grabbed it yet, we didn't want it */ + } + + /* We got out of the loop, so we have the first part of the first + revision delta in KEY (the revision) and VALUE (the date key + and its value). This is what getdelta expects to receive. */ + + while ((vnode = getdelta (&rcsbuf, rcsfile, &key, &value)) != NULL) + { + /* get the node */ + q = getnode (); + q->type = RCSVERS; + q->delproc = rcsvers_delproc; + q->data = (char *) vnode; + q->key = vnode->version; + + /* add the nodes to the list */ + if (addnode (rdata->versions, q) != 0) + { +#if 0 + purify_printf("WARNING: Adding duplicate version: %s (%s)\n", + q->key, rcsfile); + freenode (q); +#endif + } + } + + /* Here KEY and VALUE are whatever caused getdelta to return NULL. */ + + if (STREQ (key, RCSDESC)) + { + if (rdata->desc != NULL) + { + error (0, 0, + "warning: duplicate key `%s' in RCS file `%s'", + key, rcsfile); + free (rdata->desc); + } + /* Don't need to rcsbuf_valcopy `value' because + getdelta already did that. */ + rdata->desc = xstrdup (value); + } + + rdata->delta_pos = rcsbuf_ftell (&rcsbuf); + + if (pfp == NULL) + rcsbuf_cache (rdata, &rcsbuf); + else + { + *pfp = fp; + *rcsbufp = rcsbuf; + } + rdata->flags &= ~PARTIAL; +} + +/* Move RCS into or out of the Attic, depending on TOATTIC. If the + file is already in the desired place, return without doing + anything. At some point may want to think about how this relates + to RCS_rewrite but that is a bit hairy (if one wants renames to be + atomic, or that kind of thing). If there is an error, print a message + and return 1. On success, return 0. */ +int +RCS_setattic (rcs, toattic) + RCSNode *rcs; + int toattic; +{ + char *newpath; + char *p; + char *q; + + /* Some systems aren't going to let us rename an open file. */ + rcsbuf_cache_close (); + + /* Could make the pathname computations in this file, and probably + in other parts of rcs.c too, easier if the REPOS and FILE + arguments to RCS_parse got stashed in the RCSNode. */ + + if (toattic) + { + mode_t omask; + + if (rcs->flags & INATTIC) + return 0; + + /* Example: rcs->path is "/foo/bar/baz,v". */ + newpath = xmalloc (strlen (rcs->path) + sizeof CVSATTIC + 5); + p = last_component (rcs->path); + strncpy (newpath, rcs->path, p - rcs->path); + strcpy (newpath + (p - rcs->path), CVSATTIC); + + /* Create the Attic directory if it doesn't exist. */ + omask = umask (cvsumask); + if (CVS_MKDIR (newpath, 0777) < 0 && errno != EEXIST) + error (0, errno, "cannot make directory %s", newpath); + (void) umask (omask); + + strcat (newpath, "/"); + strcat (newpath, p); + + if (CVS_RENAME (rcs->path, newpath) < 0) + { + int save_errno = errno; + + /* The checks for isreadable look awfully fishy, but + I'm going to leave them here for now until I + can think harder about whether they take care of + some cases which should be handled somehow. */ + + if (isreadable (rcs->path) || !isreadable (newpath)) + { + error (0, save_errno, "cannot rename %s to %s", + rcs->path, newpath); + free (newpath); + return 1; + } + } + } + else + { + if (!(rcs->flags & INATTIC)) + return 0; + + newpath = xmalloc (strlen (rcs->path)); + + /* Example: rcs->path is "/foo/bar/Attic/baz,v". */ + p = last_component (rcs->path); + strncpy (newpath, rcs->path, p - rcs->path - 1); + newpath[p - rcs->path - 1] = '\0'; + q = newpath + (p - rcs->path - 1) - (sizeof CVSATTIC - 1); + assert (strncmp (q, CVSATTIC, sizeof CVSATTIC - 1) == 0); + strcpy (q, p); + + if (CVS_RENAME (rcs->path, newpath) < 0) + { + error (0, errno, "failed to move `%s' out of the attic", + rcs->path); + free (newpath); + return 1; + } + } + + free (rcs->path); + rcs->path = newpath; + + return 0; +} + +/* + * Fully parse the RCS file. Store all keyword/value pairs, fetch the + * log messages for each revision, and fetch add and delete counts for + * each revision (we could fetch the entire text for each revision, + * but the only caller, log_fileproc, doesn't need that information, + * so we don't waste the memory required to store it). The add and + * delete counts are stored on the OTHER field of the RCSVERSNODE + * structure, under the names ";add" and ";delete", so that we don't + * waste the memory space of extra fields in RCSVERSNODE for code + * which doesn't need this information. + */ + +void +RCS_fully_parse (rcs) + RCSNode *rcs; +{ + FILE *fp; + struct rcsbuffer rcsbuf; + + RCS_reparsercsfile (rcs, &fp, &rcsbuf); + + while (1) + { + char *key, *value; + Node *vers; + RCSVers *vnode; + + /* Rather than try to keep track of how much information we + have read, just read to the end of the file. */ + if (! rcsbuf_getrevnum (&rcsbuf, &key)) + break; + + vers = findnode (rcs->versions, key); + if (vers == NULL) + error (1, 0, + "mismatch in rcs file %s between deltas and deltatexts", + rcs->path); + + vnode = (RCSVers *) vers->data; + + while (rcsbuf_getkey (&rcsbuf, &key, &value)) + { + if (! STREQ (key, "text")) + { + Node *kv; + + if (vnode->other == NULL) + vnode->other = getlist (); + kv = getnode (); + kv->type = RCSFIELD; + kv->key = xstrdup (key); + kv->data = rcsbuf_valcopy (&rcsbuf, value, 1, (size_t *) NULL); + if (addnode (vnode->other, kv) != 0) + { + error (0, 0, + "\ +warning: duplicate key `%s' in version `%s' of RCS file `%s'", + key, vnode->version, rcs->path); + freenode (kv); + } + + continue; + } + + if (! STREQ (vnode->version, rcs->head)) + { + unsigned long add, del; + char buf[50]; + Node *kv; + + /* This is a change text. Store the add and delete + counts. */ + add = 0; + del = 0; + if (value != NULL) + { + size_t vallen; + const char *cp; + + rcsbuf_valpolish (&rcsbuf, value, 0, &vallen); + cp = value; + while (cp < value + vallen) + { + char op; + unsigned long count; + + op = *cp++; + if (op != 'a' && op != 'd') + error (1, 0, "\ +unrecognized operation '\\x%x' in %s", + op, rcs->path); + (void) strtoul (cp, (char **) &cp, 10); + if (*cp++ != ' ') + error (1, 0, "space expected in %s", + rcs->path); + count = strtoul (cp, (char **) &cp, 10); + if (*cp++ != '\012') + error (1, 0, "linefeed expected in %s", + rcs->path); + + if (op == 'd') + del += count; + else + { + add += count; + while (count != 0) + { + if (*cp == '\012') + --count; + else if (cp == value + vallen) + { + if (count != 1) + error (1, 0, "\ +invalid rcs file %s: premature end of value", + rcs->path); + else + break; + } + ++cp; + } + } + } + } + + sprintf (buf, "%lu", add); + kv = getnode (); + kv->type = RCSFIELD; + kv->key = xstrdup (";add"); + kv->data = xstrdup (buf); + if (addnode (vnode->other, kv) != 0) + { + error (0, 0, + "\ +warning: duplicate key `%s' in version `%s' of RCS file `%s'", + key, vnode->version, rcs->path); + freenode (kv); + } + + sprintf (buf, "%lu", del); + kv = getnode (); + kv->type = RCSFIELD; + kv->key = xstrdup (";delete"); + kv->data = xstrdup (buf); + if (addnode (vnode->other, kv) != 0) + { + error (0, 0, + "\ +warning: duplicate key `%s' in version `%s' of RCS file `%s'", + key, vnode->version, rcs->path); + freenode (kv); + } + } + + /* We have found the "text" key which ends the data for + this revision. Break out of the loop and go on to the + next revision. */ + break; + } + } + + rcsbuf_cache (rcs, &rcsbuf); +} + +/* + * freercsnode - free up the info for an RCSNode + */ +void +freercsnode (rnodep) + RCSNode **rnodep; +{ + if (rnodep == NULL || *rnodep == NULL) + return; + + ((*rnodep)->refcount)--; + if ((*rnodep)->refcount != 0) + { + *rnodep = (RCSNode *) NULL; + return; + } + free ((*rnodep)->path); + if ((*rnodep)->head != (char *) NULL) + free ((*rnodep)->head); + if ((*rnodep)->branch != (char *) NULL) + free ((*rnodep)->branch); + free_rcsnode_contents (*rnodep); + free ((char *) *rnodep); + *rnodep = (RCSNode *) NULL; +} + +/* + * free_rcsnode_contents - free up the contents of an RCSNode without + * freeing the node itself, or the file name, or the head, or the + * path. This returns the RCSNode to the state it is in immediately + * after a call to RCS_parse. + */ +static void +free_rcsnode_contents (rnode) + RCSNode *rnode; +{ + dellist (&rnode->versions); + if (rnode->symbols != (List *) NULL) + dellist (&rnode->symbols); + if (rnode->symbols_data != (char *) NULL) + free (rnode->symbols_data); + if (rnode->expand != NULL) + free (rnode->expand); + if (rnode->other != (List *) NULL) + dellist (&rnode->other); + if (rnode->access != NULL) + free (rnode->access); + if (rnode->locks_data != NULL) + free (rnode->locks_data); + if (rnode->locks != (List *) NULL) + dellist (&rnode->locks); + if (rnode->comment != NULL) + free (rnode->comment); + if (rnode->desc != NULL) + free (rnode->desc); +} + +/* free_rcsvers_contents -- free up the contents of an RCSVers node, + but also free the pointer to the node itself. */ +/* Note: The `hardlinks' list is *not* freed, since it is merely a + pointer into the `hardlist' structure (defined in hardlink.c), and + that structure is freed elsewhere in the program. */ + +static void +free_rcsvers_contents (rnode) + RCSVers *rnode; +{ + if (rnode->branches != (List *) NULL) + dellist (&rnode->branches); + if (rnode->date != (char *) NULL) + free (rnode->date); + if (rnode->next != (char *) NULL) + free (rnode->next); + if (rnode->author != (char *) NULL) + free (rnode->author); + if (rnode->state != (char *) NULL) + free (rnode->state); + if (rnode->other != (List *) NULL) + dellist (&rnode->other); + if (rnode->other_delta != NULL) + dellist (&rnode->other_delta); + if (rnode->text != NULL) + freedeltatext (rnode->text); + free ((char *) rnode); +} + +/* + * rcsvers_delproc - free up an RCSVers type node + */ +static void +rcsvers_delproc (p) + Node *p; +{ + free_rcsvers_contents ((RCSVers *) p->data); +} + +/* These functions retrieve keys and values from an RCS file using a + buffer. We use this somewhat complex approach because it turns out + that for many common operations, CVS spends most of its time + reading keys, so it's worth doing some fairly hairy optimization. */ + +/* The number of bytes we try to read each time we need more data. */ + +#define RCSBUF_BUFSIZE (8192) + +/* The buffer we use to store data. This grows as needed. */ + +static char *rcsbuf_buffer = NULL; +static size_t rcsbuf_buffer_size = 0; + +/* Whether rcsbuf_buffer is in use. This is used as a sanity check. */ + +static int rcsbuf_inuse; + +/* Set up to start gathering keys and values from an RCS file. This + initializes RCSBUF. */ + +static void +rcsbuf_open (rcsbuf, fp, filename, pos) + struct rcsbuffer *rcsbuf; + FILE *fp; + const char *filename; + unsigned long pos; +{ + if (rcsbuf_inuse) + error (1, 0, "rcsbuf_open: internal error"); + rcsbuf_inuse = 1; + + if (rcsbuf_buffer_size < RCSBUF_BUFSIZE) + expand_string (&rcsbuf_buffer, &rcsbuf_buffer_size, RCSBUF_BUFSIZE); + + rcsbuf->ptr = rcsbuf_buffer; + rcsbuf->ptrend = rcsbuf_buffer; + rcsbuf->fp = fp; + rcsbuf->filename = filename; + rcsbuf->pos = pos; + rcsbuf->vlen = 0; + rcsbuf->at_string = 0; + rcsbuf->embedded_at = 0; +} + +/* Stop gathering keys from an RCS file. */ + +static void +rcsbuf_close (rcsbuf) + struct rcsbuffer *rcsbuf; +{ + if (! rcsbuf_inuse) + error (1, 0, "rcsbuf_close: internal error"); + rcsbuf_inuse = 0; +} + +/* Read a key/value pair from an RCS file. This sets *KEYP to point + to the key, and *VALUEP to point to the value. A missing or empty + value is indicated by setting *VALUEP to NULL. + + This function returns 1 on success, or 0 on EOF. If there is an + error reading the file, or an EOF in an unexpected location, it + gives a fatal error. + + This sets *KEYP and *VALUEP to point to storage managed by + rcsbuf_getkey. Moreover, *VALUEP has not been massaged from the + RCS format: it may contain embedded whitespace and embedded '@' + characters. Call rcsbuf_valcopy or rcsbuf_valpolish to do + appropriate massaging. */ + +static int +rcsbuf_getkey (rcsbuf, keyp, valp) + struct rcsbuffer *rcsbuf; + char **keyp; + char **valp; +{ + register const char * const my_spacetab = spacetab; + register char *ptr, *ptrend; + char c; + +#define my_whitespace(c) (my_spacetab[(unsigned char)c] != 0) + + rcsbuf->vlen = 0; + rcsbuf->at_string = 0; + rcsbuf->embedded_at = 0; + + ptr = rcsbuf->ptr; + ptrend = rcsbuf->ptrend; + + /* Sanity check. */ + if (ptr < rcsbuf_buffer || ptr > rcsbuf_buffer + rcsbuf_buffer_size) + abort (); + + /* If the pointer is more than RCSBUF_BUFSIZE bytes into the + buffer, move back to the start of the buffer. This keeps the + buffer from growing indefinitely. */ + if (ptr - rcsbuf_buffer >= RCSBUF_BUFSIZE) + { + int len; + + len = ptrend - ptr; + + /* Sanity check: we don't read more than RCSBUF_BUFSIZE bytes + at a time, so we can't have more bytes than that past PTR. */ + if (len > RCSBUF_BUFSIZE) + abort (); + + /* Update the POS field, which holds the file offset of the + first byte in the RCSBUF_BUFFER buffer. */ + rcsbuf->pos += ptr - rcsbuf_buffer; + + memcpy (rcsbuf_buffer, ptr, len); + ptr = rcsbuf_buffer; + ptrend = ptr + len; + rcsbuf->ptrend = ptrend; + } + + /* Skip leading whitespace. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, (char **) NULL); + if (ptr == NULL) + return 0; + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + if (! my_whitespace (c)) + break; + + ++ptr; + } + + /* We've found the start of the key. */ + + *keyp = ptr; + + if (c != ';') + { + while (1) + { + ++ptr; + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, keyp, (char **) NULL); + if (ptr == NULL) + error (1, 0, "EOF in key in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + c = *ptr; + if (c == ';' || my_whitespace (c)) + break; + } + } + + /* Here *KEYP points to the key in the buffer, C is the character + we found at the of the key, and PTR points to the location in + the buffer where we found C. We must set *PTR to \0 in order + to terminate the key. If the key ended with ';', then there is + no value. */ + + *ptr = '\0'; + ++ptr; + + if (c == ';') + { + *valp = NULL; + rcsbuf->ptr = ptr; + return 1; + } + + /* C must be whitespace. Skip whitespace between the key and the + value. If we find ';' now, there is no value. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, keyp, (char **) NULL); + if (ptr == NULL) + error (1, 0, "EOF while looking for value in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + c = *ptr; + if (c == ';') + { + *valp = NULL; + rcsbuf->ptr = ptr + 1; + return 1; + } + if (! my_whitespace (c)) + break; + ++ptr; + } + + /* Now PTR points to the start of the value, and C is the first + character of the value. */ + + if (c != '@') + *valp = ptr; + else + { + char *pat; + size_t vlen; + + /* Optimize the common case of a value composed of a single + '@' string. */ + + rcsbuf->at_string = 1; + + ++ptr; + + *valp = ptr; + + while (1) + { + while ((pat = memchr (ptr, '@', ptrend - ptr)) == NULL) + { + /* Note that we pass PTREND as the PTR value to + rcsbuf_fill, so that we will wind up setting PTR to + the location corresponding to the old PTREND, so + that we don't search the same bytes again. */ + ptr = rcsbuf_fill (rcsbuf, ptrend, keyp, valp); + if (ptr == NULL) + error (1, 0, + "EOF while looking for end of string in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + /* Handle the special case of an '@' right at the end of + the known bytes. */ + if (pat + 1 >= ptrend) + { + /* Note that we pass PAT, not PTR, here. */ + pat = rcsbuf_fill (rcsbuf, pat, keyp, valp); + if (pat == NULL) + { + /* EOF here is OK; it just means that the last + character of the file was an '@' terminating a + value for a key type which does not require a + trailing ';'. */ + pat = rcsbuf->ptrend - 1; + + } + ptrend = rcsbuf->ptrend; + + /* Note that the value of PTR is bogus here. This is + OK, because we don't use it. */ + } + + if (pat + 1 >= ptrend || pat[1] != '@') + break; + + /* We found an '@' pair in the string. Keep looking. */ + ++rcsbuf->embedded_at; + ptr = pat + 2; + } + + /* Here PAT points to the final '@' in the string. */ + + *pat = '\0'; + + vlen = pat - *valp; + if (vlen == 0) + *valp = NULL; + rcsbuf->vlen = vlen; + + ptr = pat + 1; + } + + /* Certain keywords only have a '@' string. If there is no '@' + string, then the old getrcskey function assumed that they had + no value, and we do the same. */ + + { + char *k; + + k = *keyp; + if (STREQ (k, RCSDESC) + || STREQ (k, "text") + || STREQ (k, "log")) + { + if (c != '@') + *valp = NULL; + rcsbuf->ptr = ptr; + return 1; + } + } + + /* If we've already gathered a '@' string, try to skip whitespace + and find a ';'. */ + if (c == '@') + { + while (1) + { + char n; + + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, keyp, valp); + if (ptr == NULL) + error (1, 0, "EOF in value in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + n = *ptr; + if (n == ';') + { + /* We're done. We already set everything up for this + case above. */ + rcsbuf->ptr = ptr + 1; + return 1; + } + if (! my_whitespace (n)) + break; + ++ptr; + } + + /* The value extends past the '@' string. We need to undo the + closing of the '@' done in the default case above. This + case never happens in a plain RCS file, but it can happen + if user defined phrases are used. */ + if (rcsbuf->vlen != 0) + (*valp)[rcsbuf->vlen] = ' '; + else + *valp = ptr; + } + + /* Here we have a value which is not a simple '@' string. We need + to gather up everything until the next ';', including any '@' + strings. *VALP points to the start of the value. If + RCSBUF->VLEN is not zero, then we have already read an '@' + string, and PTR points to the data following the '@' string. + Otherwise, PTR points to the start of the value. */ + + while (1) + { + char *start, *psemi, *pat; + + /* Find the ';' which must end the value. */ + start = ptr; + while ((psemi = memchr (ptr, ';', ptrend - ptr)) == NULL) + { + int slen; + + /* Note that we pass PTREND as the PTR value to + rcsbuf_fill, so that we will wind up setting PTR to the + location corresponding to the old PTREND, so that we + don't search the same bytes again. */ + slen = start - *valp; + ptr = rcsbuf_fill (rcsbuf, ptrend, keyp, valp); + if (ptr == NULL) + error (1, 0, "EOF in value in RCS file %s", rcsbuf->filename); + start = *valp + slen; + ptrend = rcsbuf->ptrend; + } + + /* See if there are any '@' strings in the value. */ + pat = memchr (start, '@', psemi - start); + + if (pat == NULL) + { + size_t vlen; + + /* We're done with the value. Trim any trailing + whitespace. */ + + rcsbuf->ptr = psemi + 1; + + start = *valp; + while (psemi > start && my_whitespace (psemi[-1])) + --psemi; + *psemi = '\0'; + + vlen = psemi - start; + if (vlen == 0) + *valp = NULL; + rcsbuf->vlen = vlen; + + return 1; + } + + /* We found an '@' string in the value. We set + RCSBUF->AT_STRING, which means that we won't be able to + compress whitespace correctly for this type of value. + Since this type of value never arises in a normal RCS file, + this should not be a big deal. It means that if anybody + adds a phrase which can have both an '@' string and regular + text, they will have to handle whitespace compression + themselves. */ + + rcsbuf->at_string = 1; + + *pat = ' '; + + ptr = pat + 1; + + while (1) + { + while ((pat = memchr (ptr, '@', ptrend - ptr)) == NULL) + { + /* Note that we pass PTREND as the PTR value to + rcsbuff_fill, so that we will wind up setting PTR + to the location corresponding to the old PTREND, so + that we don't search the same bytes again. */ + ptr = rcsbuf_fill (rcsbuf, ptrend, keyp, valp); + if (ptr == NULL) + error (1, 0, + "EOF while looking for end of string in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + /* Handle the special case of an '@' right at the end of + the known bytes. */ + if (pat + 1 >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, keyp, valp); + if (ptr == NULL) + error (1, 0, "EOF in value in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + if (pat[1] != '@') + break; + + /* We found an '@' pair in the string. Keep looking. */ + ++rcsbuf->embedded_at; + ptr = pat + 2; + } + + /* Here PAT points to the final '@' in the string. */ + + *pat = ' '; + + ptr = pat + 1; + } + +#undef my_whitespace +} + +/* TODO: Eliminate redundant code in rcsbuf_getkey, rcsbuf_getid, + rcsbuf_getstring, rcsbuf_getword. These last three functions were + all created by hacking monstrous swaths of code from rcsbuf_getkey, + and some engineering would make the code easier to read and + maintain. + + Note that the extreme hair in rcsbuf_getkey is because profiling + statistics show that it was worth it. + + We probably could be processing "hardlinks" by first calling + rcsbuf_getkey, and breaking up the value afterwards; the code to + break it up would not need to be hacked for speed. This would + remove the need for rcsbuf_getword, rcsbuf_getid, and + rcsbuf_getstring, as the other calls are easy to remove. */ + +/* Read an `id' (in the sense of rcsfile(5)) from RCSBUF, and store in + IDP. */ + +static int +rcsbuf_getid (rcsbuf, idp) + struct rcsbuffer *rcsbuf; + char **idp; +{ + register const char * const my_spacetab = spacetab; + register char *ptr, *ptrend; + char c; + +#define my_whitespace(c) (my_spacetab[(unsigned char)c] != 0) + + rcsbuf->vlen = 0; + rcsbuf->at_string = 0; + rcsbuf->embedded_at = 0; + + ptr = rcsbuf->ptr; + ptrend = rcsbuf->ptrend; + + /* Sanity check. */ + if (ptr < rcsbuf_buffer || ptr > rcsbuf_buffer + rcsbuf_buffer_size) + abort (); + + /* If the pointer is more than RCSBUF_BUFSIZE bytes into the + buffer, move back to the start of the buffer. This keeps the + buffer from growing indefinitely. */ + if (ptr - rcsbuf_buffer >= RCSBUF_BUFSIZE) + { + int len; + + len = ptrend - ptr; + + /* Sanity check: we don't read more than RCSBUF_BUFSIZE bytes + at a time, so we can't have more bytes than that past PTR. */ + if (len > RCSBUF_BUFSIZE) + abort (); + + /* Update the POS field, which holds the file offset of the + first byte in the RCSBUF_BUFFER buffer. */ + rcsbuf->pos += ptr - rcsbuf_buffer; + + memcpy (rcsbuf_buffer, ptr, len); + ptr = rcsbuf_buffer; + ptrend = ptr + len; + rcsbuf->ptrend = ptrend; + } + + /* Skip leading whitespace. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, (char **) NULL); + if (ptr == NULL) + return 0; + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + if (! my_whitespace (c)) + break; + + ++ptr; + } + + /* We've found the start of the key. */ + + *idp = ptr; + + if (c != ';') + { + while (1) + { + ++ptr; + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, idp, (char **) NULL); + if (ptr == NULL) + error (1, 0, "EOF in key in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + c = *ptr; + if (c == ';' || my_whitespace (c)) + break; + } + } + + /* Here *IDP points to the id in the buffer, C is the character + we found at the end of the key, and PTR points to the location in + the buffer where we found C. We may not set *PTR to \0, because + it may overwrite a terminating semicolon. The calling function + must copy and terminate the id on its own. */ + + rcsbuf->ptr = ptr; + return 1; + +#undef my_whitespace +} + +/* Read an RCS @-delimited string. Store the result in STRP. */ + +static int +rcsbuf_getstring (rcsbuf, strp) + struct rcsbuffer *rcsbuf; + char **strp; +{ + register const char * const my_spacetab = spacetab; + register char *ptr, *ptrend; + char *pat; + size_t vlen; + char c; + +#define my_whitespace(c) (my_spacetab[(unsigned char)c] != 0) + + rcsbuf->vlen = 0; + rcsbuf->at_string = 0; + rcsbuf->embedded_at = 0; + + ptr = rcsbuf->ptr; + ptrend = rcsbuf->ptrend; + + /* Sanity check. */ + if (ptr < rcsbuf_buffer || ptr > rcsbuf_buffer + rcsbuf_buffer_size) + abort (); + + /* If the pointer is more than RCSBUF_BUFSIZE bytes into the + buffer, move back to the start of the buffer. This keeps the + buffer from growing indefinitely. */ + if (ptr - rcsbuf_buffer >= RCSBUF_BUFSIZE) + { + int len; + + len = ptrend - ptr; + + /* Sanity check: we don't read more than RCSBUF_BUFSIZE bytes + at a time, so we can't have more bytes than that past PTR. */ + if (len > RCSBUF_BUFSIZE) + abort (); + + /* Update the POS field, which holds the file offset of the + first byte in the RCSBUF_BUFFER buffer. */ + rcsbuf->pos += ptr - rcsbuf_buffer; + + memcpy (rcsbuf_buffer, ptr, len); + ptr = rcsbuf_buffer; + ptrend = ptr + len; + rcsbuf->ptrend = ptrend; + } + + /* Skip leading whitespace. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, (char **) NULL); + if (ptr == NULL) + error (1, 0, "unexpected end of file reading %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + if (! my_whitespace (c)) + break; + + ++ptr; + } + + /* PTR should now point to the start of a string. */ + if (c != '@') + error (1, 0, "expected @-string at '\\x%x' in %s", + c, rcsbuf->filename); + + /* Optimize the common case of a value composed of a single + '@' string. */ + + rcsbuf->at_string = 1; + + ++ptr; + + *strp = ptr; + + while (1) + { + while ((pat = memchr (ptr, '@', ptrend - ptr)) == NULL) + { + /* Note that we pass PTREND as the PTR value to + rcsbuf_fill, so that we will wind up setting PTR to + the location corresponding to the old PTREND, so + that we don't search the same bytes again. */ + ptr = rcsbuf_fill (rcsbuf, ptrend, NULL, strp); + if (ptr == NULL) + error (1, 0, + "EOF while looking for end of string in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + /* Handle the special case of an '@' right at the end of + the known bytes. */ + if (pat + 1 >= ptrend) + { + /* Note that we pass PAT, not PTR, here. */ + pat = rcsbuf_fill (rcsbuf, pat, NULL, strp); + if (pat == NULL) + { + /* EOF here is OK; it just means that the last + character of the file was an '@' terminating a + value for a key type which does not require a + trailing ';'. */ + pat = rcsbuf->ptrend - 1; + + } + ptrend = rcsbuf->ptrend; + + /* Note that the value of PTR is bogus here. This is + OK, because we don't use it. */ + } + + if (pat + 1 >= ptrend || pat[1] != '@') + break; + + /* We found an '@' pair in the string. Keep looking. */ + ++rcsbuf->embedded_at; + ptr = pat + 2; + } + + /* Here PAT points to the final '@' in the string. */ + + *pat = '\0'; + + vlen = pat - *strp; + if (vlen == 0) + *strp = NULL; + rcsbuf->vlen = vlen; + rcsbuf->ptr = pat + 1; + + return 1; + +#undef my_whitespace +} + +/* Read an RCS `word', in the sense of rcsfile(5) (an id, a num, a + @-delimited string, or `:'). Store the result in WORDP. If a + `;' is reached without reading any text, the result is NULL. */ + +static int +rcsbuf_getword (rcsbuf, wordp) + struct rcsbuffer *rcsbuf; + char **wordp; +{ + register const char * const my_spacetab = spacetab; + register char *ptr, *ptrend; + char c; + +#define my_whitespace(c) (my_spacetab[(unsigned char)c] != 0) + + rcsbuf->vlen = 0; + rcsbuf->at_string = 0; + rcsbuf->embedded_at = 0; + + ptr = rcsbuf->ptr; + ptrend = rcsbuf->ptrend; + + /* Sanity check. */ + if (ptr < rcsbuf_buffer || ptr > rcsbuf_buffer + rcsbuf_buffer_size) + abort (); + + /* If the pointer is more than RCSBUF_BUFSIZE bytes into the + buffer, move back to the start of the buffer. This keeps the + buffer from growing indefinitely. */ + if (ptr - rcsbuf_buffer >= RCSBUF_BUFSIZE) + { + int len; + + len = ptrend - ptr; + + /* Sanity check: we don't read more than RCSBUF_BUFSIZE bytes + at a time, so we can't have more bytes than that past PTR. */ + if (len > RCSBUF_BUFSIZE) + abort (); + + /* Update the POS field, which holds the file offset of the + first byte in the RCSBUF_BUFFER buffer. */ + rcsbuf->pos += ptr - rcsbuf_buffer; + + memcpy (rcsbuf_buffer, ptr, len); + ptr = rcsbuf_buffer; + ptrend = ptr + len; + rcsbuf->ptrend = ptrend; + } + + /* Skip leading whitespace. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, (char **) NULL); + if (ptr == NULL) + error (1, 0, "unexpected end of file reading %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + if (! my_whitespace (c)) + break; + + ++ptr; + } + + /* If we have reached `;', there is no value. */ + if (c == ';') + { + *wordp = NULL; + *ptr++ = '\0'; + rcsbuf->ptr = ptr; + rcsbuf->vlen = 0; + return 1; + } + + /* PTR now points to the start of a value. Find out whether it is + a num, an id, a string or a colon. */ + if (c == ':') + { + *wordp = ptr++; + rcsbuf->ptr = ptr; + rcsbuf->vlen = 1; + return 1; + } + + if (c == '@') + { + char *pat; + size_t vlen; + + /* Optimize the common case of a value composed of a single + '@' string. */ + + rcsbuf->at_string = 1; + + ++ptr; + + *wordp = ptr; + + while (1) + { + while ((pat = memchr (ptr, '@', ptrend - ptr)) == NULL) + { + /* Note that we pass PTREND as the PTR value to + rcsbuf_fill, so that we will wind up setting PTR to + the location corresponding to the old PTREND, so + that we don't search the same bytes again. */ + ptr = rcsbuf_fill (rcsbuf, ptrend, NULL, wordp); + if (ptr == NULL) + error (1, 0, + "EOF while looking for end of string in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + /* Handle the special case of an '@' right at the end of + the known bytes. */ + if (pat + 1 >= ptrend) + { + /* Note that we pass PAT, not PTR, here. */ + pat = rcsbuf_fill (rcsbuf, pat, NULL, wordp); + if (pat == NULL) + { + /* EOF here is OK; it just means that the last + character of the file was an '@' terminating a + value for a key type which does not require a + trailing ';'. */ + pat = rcsbuf->ptrend - 1; + + } + ptrend = rcsbuf->ptrend; + + /* Note that the value of PTR is bogus here. This is + OK, because we don't use it. */ + } + + if (pat + 1 >= ptrend || pat[1] != '@') + break; + + /* We found an '@' pair in the string. Keep looking. */ + ++rcsbuf->embedded_at; + ptr = pat + 2; + } + + /* Here PAT points to the final '@' in the string. */ + + *pat = '\0'; + + vlen = pat - *wordp; + if (vlen == 0) + *wordp = NULL; + rcsbuf->vlen = vlen; + rcsbuf->ptr = pat + 1; + + return 1; + } + + /* C is neither `:', `;' nor `@', so it should be the start of a num + or an id. Make sure it is not another special character. */ + if (c == '$' || c == '.' || c == ',') + { + error (1, 0, "illegal special character in RCS field in %s", + rcsbuf->filename); + } + + *wordp = ptr; + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, wordp); + if (ptr == NULL) + error (1, 0, "unexpected end of file reading %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + /* Legitimate ID characters are digits, dots and any `graphic + printing character that is not a special.' This test ought + to do the trick. */ + c = *ptr; + if (isprint ((unsigned char) c) && + c != ';' && c != '$' && c != ',' && c != '@' && c != ':') + { + ++ptr; + continue; + } + break; + } + + /* PTR points to the last non-id character in this word, and C is + the character in its memory cell. Check to make sure that it + is a legitimate word delimiter -- whitespace or semicolon. */ + if (c == ';' || my_whitespace (c)) + { + rcsbuf->vlen = ptr - *wordp; + rcsbuf->ptr = ptr; + return 1; + } + + error (1, 0, "illegal special character in RCS field in %s", + rcsbuf->filename); + /* Shut up compiler warnings. */ + return 0; + +#undef my_whitespace +} + +/* Read an RCS revision number from an RCS file. This sets *REVP to + point to the revision number; it will point to space that is + managed by the rcsbuf functions, and is only good until the next + call to rcsbuf_getkey or rcsbuf_getrevnum. + + This function returns 1 on success, or 0 on EOF. If there is an + error reading the file, or an EOF in an unexpected location, it + gives a fatal error. */ + +static int +rcsbuf_getrevnum (rcsbuf, revp) + struct rcsbuffer *rcsbuf; + char **revp; +{ + char *ptr, *ptrend; + char c; + + ptr = rcsbuf->ptr; + ptrend = rcsbuf->ptrend; + + *revp = NULL; + + /* Skip leading whitespace. */ + + while (1) + { + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, (char **) NULL, (char **) NULL); + if (ptr == NULL) + return 0; + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + if (! whitespace (c)) + break; + + ++ptr; + } + + if (! isdigit ((unsigned char) c) && c != '.') + error (1, 0, + "\ +unexpected '\\x%x' reading revision number in RCS file %s", + c, rcsbuf->filename); + + *revp = ptr; + + do + { + ++ptr; + if (ptr >= ptrend) + { + ptr = rcsbuf_fill (rcsbuf, ptr, revp, (char **) NULL); + if (ptr == NULL) + error (1, 0, + "unexpected EOF reading revision number in RCS file %s", + rcsbuf->filename); + ptrend = rcsbuf->ptrend; + } + + c = *ptr; + } + while (isdigit ((unsigned char) c) || c == '.'); + + if (! whitespace (c)) + error (1, 0, "\ +unexpected '\\x%x' reading revision number in RCS file %s", + c, rcsbuf->filename); + + *ptr = '\0'; + + rcsbuf->ptr = ptr + 1; + + return 1; +} + +/* Fill RCSBUF_BUFFER with bytes from the file associated with RCSBUF, + updating PTR and the PTREND field. If KEYP and *KEYP are not NULL, + then *KEYP points into the buffer, and must be adjusted if the + buffer is changed. Likewise for VALP. Returns the new value of + PTR, or NULL on error. */ + +static char * +rcsbuf_fill (rcsbuf, ptr, keyp, valp) + struct rcsbuffer *rcsbuf; + char *ptr; + char **keyp; + char **valp; +{ + int got; + + if (rcsbuf->ptrend - rcsbuf_buffer + RCSBUF_BUFSIZE > rcsbuf_buffer_size) + { + int poff, peoff, koff, voff; + + poff = ptr - rcsbuf_buffer; + peoff = rcsbuf->ptrend - rcsbuf_buffer; + if (keyp != NULL && *keyp != NULL) + koff = *keyp - rcsbuf_buffer; + if (valp != NULL && *valp != NULL) + voff = *valp - rcsbuf_buffer; + + expand_string (&rcsbuf_buffer, &rcsbuf_buffer_size, + rcsbuf_buffer_size + RCSBUF_BUFSIZE); + + ptr = rcsbuf_buffer + poff; + rcsbuf->ptrend = rcsbuf_buffer + peoff; + if (keyp != NULL && *keyp != NULL) + *keyp = rcsbuf_buffer + koff; + if (valp != NULL && *valp != NULL) + *valp = rcsbuf_buffer + voff; + } + + got = fread (rcsbuf->ptrend, 1, RCSBUF_BUFSIZE, rcsbuf->fp); + if (got == 0) + { + if (ferror (rcsbuf->fp)) + error (1, errno, "cannot read %s", rcsbuf->filename); + return NULL; + } + + rcsbuf->ptrend += got; + + return ptr; +} + +/* Copy the value VAL returned by rcsbuf_getkey into a memory buffer, + returning the memory buffer. Polish the value like + rcsbuf_valpolish, q.v. */ + +static char * +rcsbuf_valcopy (rcsbuf, val, polish, lenp) + struct rcsbuffer *rcsbuf; + char *val; + int polish; + size_t *lenp; +{ + size_t vlen; + int embedded_at; + char *ret; + + if (val == NULL) + { + if (lenp != NULL) + *lenp = 0; + return NULL; + } + + vlen = rcsbuf->vlen; + embedded_at = rcsbuf->embedded_at; + + ret = xmalloc (vlen - embedded_at + 1); + + if (rcsbuf->at_string ? embedded_at == 0 : ! polish) + { + /* No special action to take. */ + memcpy (ret, val, vlen + 1); + if (lenp != NULL) + *lenp = vlen; + return ret; + } + + rcsbuf_valpolish_internal (rcsbuf, ret, val, lenp); + return ret; +} + +/* Polish the value VAL returned by rcsbuf_getkey. The POLISH + parameter is non-zero if multiple embedded whitespace characters + should be compressed into a single whitespace character. Note that + leading and trailing whitespace was already removed by + rcsbuf_getkey. Within an '@' string, pairs of '@' characters are + compressed into a single '@' character regardless of the value of + POLISH. If LENP is not NULL, set *LENP to the length of the value. */ + +static void +rcsbuf_valpolish (rcsbuf, val, polish, lenp) + struct rcsbuffer *rcsbuf; + char *val; + int polish; + size_t *lenp; +{ + if (val == NULL) + { + if (lenp != NULL) + *lenp= 0; + return; + } + + if (rcsbuf->at_string ? rcsbuf->embedded_at == 0 : ! polish) + { + /* No special action to take. */ + if (lenp != NULL) + *lenp = rcsbuf->vlen; + return; + } + + rcsbuf_valpolish_internal (rcsbuf, val, val, lenp); +} + +/* Internal polishing routine, called from rcsbuf_valcopy and + rcsbuf_valpolish. */ + +static void +rcsbuf_valpolish_internal (rcsbuf, to, from, lenp) + struct rcsbuffer *rcsbuf; + char *to; + const char *from; + size_t *lenp; +{ + size_t len; + + len = rcsbuf->vlen; + + if (! rcsbuf->at_string) + { + char *orig_to; + size_t clen; + + orig_to = to; + + for (clen = len; clen > 0; ++from, --clen) + { + char c; + + c = *from; + if (whitespace (c)) + { + /* Note that we know that clen can not drop to zero + while we have whitespace, because we know there is + no trailing whitespace. */ + while (whitespace (from[1])) + { + ++from; + --clen; + } + c = ' '; + } + *to++ = c; + } + + *to = '\0'; + + if (lenp != NULL) + *lenp = to - orig_to; + } + else + { + const char *orig_from; + char *orig_to; + int embedded_at; + size_t clen; + + orig_from = from; + orig_to = to; + + embedded_at = rcsbuf->embedded_at; + + if (lenp != NULL) + *lenp = len - embedded_at; + + for (clen = len; clen > 0; ++from, --clen) + { + char c; + + c = *from; + *to++ = c; + if (c == '@') + { + ++from; + + /* Sanity check. */ + if (*from != '@' || clen == 0) + abort (); + + --clen; + + --embedded_at; + if (embedded_at == 0) + { + /* We've found all the embedded '@' characters. + We can just memcpy the rest of the buffer after + this '@' character. */ + if (orig_to != orig_from) + memcpy (to, from + 1, clen - 1); + else + memmove (to, from + 1, clen - 1); + from += clen; + to += clen - 1; + break; + } + } + } + + /* Sanity check. */ + if (from != orig_from + len + || to != orig_to + (len - rcsbuf->embedded_at)) + { + abort (); + } + + *to = '\0'; + } +} + +/* Return the current position of an rcsbuf. */ + +static unsigned long +rcsbuf_ftell (rcsbuf) + struct rcsbuffer *rcsbuf; +{ + return rcsbuf->pos + (rcsbuf->ptr - rcsbuf_buffer); +} + +/* Return a pointer to any data buffered for RCSBUF, along with the + length. */ + +static void +rcsbuf_get_buffered (rcsbuf, datap, lenp) + struct rcsbuffer *rcsbuf; + char **datap; + size_t *lenp; +{ + *datap = rcsbuf->ptr; + *lenp = rcsbuf->ptrend - rcsbuf->ptr; +} + +/* CVS optimizes by quickly reading some header information from a + file. If it decides it needs to do more with the file, it reopens + it. We speed that up here by maintaining a cache of a single open + file, to save the time it takes to reopen the file in the common + case. */ + +static RCSNode *cached_rcs; +static struct rcsbuffer cached_rcsbuf; + +/* Cache RCS and RCSBUF. This takes responsibility for closing + RCSBUF->FP. */ + +static void +rcsbuf_cache (rcs, rcsbuf) + RCSNode *rcs; + struct rcsbuffer *rcsbuf; +{ + if (cached_rcs != NULL) + rcsbuf_cache_close (); + cached_rcs = rcs; + ++rcs->refcount; + cached_rcsbuf = *rcsbuf; +} + +/* If there is anything in the cache, close it. */ + +static void +rcsbuf_cache_close () +{ + if (cached_rcs != NULL) + { + if (fclose (cached_rcsbuf.fp) != 0) + error (0, errno, "cannot close %s", cached_rcsbuf.filename); + rcsbuf_close (&cached_rcsbuf); + freercsnode (&cached_rcs); + cached_rcs = NULL; + } +} + +/* Open an rcsbuffer for RCS, getting it from the cache if possible. + Set *FPP to the file, and *RCSBUFP to the rcsbuf. The file should + be put at position POS. */ + +static void +rcsbuf_cache_open (rcs, pos, pfp, prcsbuf) + RCSNode *rcs; + long pos; + FILE **pfp; + struct rcsbuffer *prcsbuf; +{ + if (cached_rcs == rcs) + { + if (rcsbuf_ftell (&cached_rcsbuf) != pos) + { + if (fseek (cached_rcsbuf.fp, pos, SEEK_SET) != 0) + error (1, 0, "cannot fseek RCS file %s", + cached_rcsbuf.filename); + cached_rcsbuf.ptr = rcsbuf_buffer; + cached_rcsbuf.ptrend = rcsbuf_buffer; + cached_rcsbuf.pos = pos; + } + *pfp = cached_rcsbuf.fp; + + /* When RCS_parse opens a file using fopen_case, it frees the + filename which we cached in CACHED_RCSBUF and stores a new + file name in RCS->PATH. We avoid problems here by always + copying the filename over. FIXME: This is hackish. */ + cached_rcsbuf.filename = rcs->path; + + *prcsbuf = cached_rcsbuf; + + cached_rcs = NULL; + + /* Removing RCS from the cache removes a reference to it. */ + --rcs->refcount; + if (rcs->refcount <= 0) + error (1, 0, "rcsbuf_cache_open: internal error"); + } + else + { + if (cached_rcs != NULL) + rcsbuf_cache_close (); + + *pfp = CVS_FOPEN (rcs->path, FOPEN_BINARY_READ); + if (*pfp == NULL) + error (1, 0, "unable to reopen `%s'", rcs->path); + if (pos != 0) + { + if (fseek (*pfp, pos, SEEK_SET) != 0) + error (1, 0, "cannot fseek RCS file %s", rcs->path); + } + rcsbuf_open (prcsbuf, *pfp, rcs->path, pos); + } +} + + +/* + * process the symbols list of the rcs file + */ +static void +do_symbols (list, val) + List *list; + char *val; +{ + Node *p; + char *cp = val; + char *tag, *rev; + + for (;;) + { + /* skip leading whitespace */ + while (whitespace (*cp)) + cp++; + + /* if we got to the end, we are done */ + if (*cp == '\0') + break; + + /* split it up into tag and rev */ + tag = cp; + cp = strchr (cp, ':'); + *cp++ = '\0'; + rev = cp; + while (!whitespace (*cp) && *cp != '\0') + cp++; + if (*cp != '\0') + *cp++ = '\0'; + + /* make a new node and add it to the list */ + p = getnode (); + p->key = xstrdup (tag); + p->data = xstrdup (rev); + (void) addnode (list, p); + } +} + +/* + * process the locks list of the rcs file + * Like do_symbols, but hash entries are keyed backwards: i.e. + * an entry like `user:rev' is keyed on REV rather than on USER. + */ +static void +do_locks (list, val) + List *list; + char *val; +{ + Node *p; + char *cp = val; + char *user, *rev; + + for (;;) + { + /* skip leading whitespace */ + while (whitespace (*cp)) + cp++; + + /* if we got to the end, we are done */ + if (*cp == '\0') + break; + + /* split it up into user and rev */ + user = cp; + cp = strchr (cp, ':'); + *cp++ = '\0'; + rev = cp; + while (!whitespace (*cp) && *cp != '\0') + cp++; + if (*cp != '\0') + *cp++ = '\0'; + + /* make a new node and add it to the list */ + p = getnode (); + p->key = xstrdup (rev); + p->data = xstrdup (user); + (void) addnode (list, p); + } +} + +/* + * process the branches list of a revision delta + */ +static void +do_branches (list, val) + List *list; + char *val; +{ + Node *p; + char *cp = val; + char *branch; + + for (;;) + { + /* skip leading whitespace */ + while (whitespace (*cp)) + cp++; + + /* if we got to the end, we are done */ + if (*cp == '\0') + break; + + /* find the end of this branch */ + branch = cp; + while (!whitespace (*cp) && *cp != '\0') + cp++; + if (*cp != '\0') + *cp++ = '\0'; + + /* make a new node and add it to the list */ + p = getnode (); + p->key = xstrdup (branch); + (void) addnode (list, p); + } +} + +/* + * Version Number + * + * Returns the requested version number of the RCS file, satisfying tags and/or + * dates, and walking branches, if necessary. + * + * The result is returned; null-string if error. + */ +char * +RCS_getversion (rcs, tag, date, force_tag_match, simple_tag) + RCSNode *rcs; + char *tag; + char *date; + int force_tag_match; + int *simple_tag; +{ + if (simple_tag != NULL) + *simple_tag = 0; + + /* make sure we have something to look at... */ + assert (rcs != NULL); + + if (tag && date) + { + char *branch, *rev; + + if (! RCS_nodeisbranch (rcs, tag)) + { + /* We can't get a particular date if the tag is not a + branch. */ + return NULL; + } + + /* Work out the branch. */ + if (! isdigit ((unsigned char) tag[0])) + branch = RCS_whatbranch (rcs, tag); + else + branch = xstrdup (tag); + + /* Fetch the revision of branch as of date. */ + rev = RCS_getdatebranch (rcs, date, branch); + free (branch); + return (rev); + } + else if (tag) + return (RCS_gettag (rcs, tag, force_tag_match, simple_tag)); + else if (date) + return (RCS_getdate (rcs, date, force_tag_match)); + else + return (RCS_head (rcs)); + +} + +/* + * Get existing revision number corresponding to tag or revision. + * Similar to RCS_gettag but less interpretation imposed. + * For example: + * -- If tag designates a magic branch, RCS_tag2rev + * returns the magic branch number. + * -- If tag is a branch tag, returns the branch number, not + * the revision of the head of the branch. + * If tag or revision is not valid or does not exist in file, + * exit with error. + */ +char * +RCS_tag2rev (rcs, tag) + RCSNode *rcs; + char *tag; +{ + char *rev, *pa, *pb; + int i; + + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* If a valid revision, try to look it up */ + if ( RCS_valid_rev (tag) ) + { + /* Make a copy so we can scribble on it */ + rev = xstrdup (tag); + + /* If revision exists, return the copy */ + if (RCS_exist_rev (rcs, tag)) + return rev; + + /* Nope, none such. If tag is not a branch we're done. */ + i = numdots (rev); + if ((i & 1) == 1 ) + { + pa = strrchr (rev, '.'); + if (i == 1 || *(pa-1) != RCS_MAGIC_BRANCH || *(pa-2) != '.') + { + free (rev); + error (1, 0, "revision `%s' does not exist", tag); + } + } + + /* Try for a real (that is, exists in the RCS deltas) branch + (RCS_exist_rev just checks for real revisions and revisions + which have tags pointing to them). */ + pa = RCS_getbranch (rcs, rev, 1); + if (pa != NULL) + { + free (pa); + return rev; + } + + /* Tag is branch, but does not exist, try corresponding + * magic branch tag. + * + * FIXME: assumes all magic branches are of + * form "n.n.n ... .0.n". I'll fix if somebody can + * send me a method to get a magic branch tag with + * the 0 in some other position -- <dan@gasboy.com> + */ + pa = strrchr (rev, '.'); + pb = xmalloc (strlen (rev) + 3); + *pa++ = 0; + (void) sprintf (pb, "%s.%d.%s", rev, RCS_MAGIC_BRANCH, pa); + free (rev); + rev = pb; + if (RCS_exist_rev (rcs, rev)) + return rev; + error (1, 0, "revision `%s' does not exist", tag); + } + + + RCS_check_tag (tag); /* exit if not a valid tag */ + + /* If tag is "HEAD", special case to get head RCS revision */ + if (tag && STREQ (tag, TAG_HEAD)) + return (RCS_head (rcs)); + + /* If valid tag let translate_symtag say yea or nay. */ + rev = translate_symtag (rcs, tag); + + if (rev) + return rev; + + error (1, 0, "tag `%s' does not exist", tag); + /* NOT REACHED -- error (1 ... ) does not return here */ + return 0; +} + +/* + * Find the revision for a specific tag. + * If force_tag_match is set, return NULL if an exact match is not + * possible otherwise return RCS_head (). We are careful to look for + * and handle "magic" revisions specially. + * + * If the matched tag is a branch tag, find the head of the branch. + * + * Returns pointer to newly malloc'd string, or NULL. + */ +char * +RCS_gettag (rcs, symtag, force_tag_match, simple_tag) + RCSNode *rcs; + char *symtag; + int force_tag_match; + int *simple_tag; +{ + char *tag = symtag; + int tag_allocated = 0; + + if (simple_tag != NULL) + *simple_tag = 0; + + /* make sure we have something to look at... */ + assert (rcs != NULL); + + /* XXX this is probably not necessary, --jtc */ + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* If tag is "HEAD", special case to get head RCS revision */ + if (tag && (STREQ (tag, TAG_HEAD) || *tag == '\0')) +#if 0 /* This #if 0 is only in the Cygnus code. Why? Death support? */ + if (force_tag_match && (rcs->flags & VALID) && (rcs->flags & INATTIC)) + return ((char *) NULL); /* head request for removed file */ + else +#endif + return (RCS_head (rcs)); + + if (!isdigit ((unsigned char) tag[0])) + { + char *version; + + /* If we got a symbolic tag, resolve it to a numeric */ + version = translate_symtag (rcs, tag); + if (version != NULL) + { + int dots; + char *magic, *branch, *cp; + + tag = version; + tag_allocated = 1; + + /* + * If this is a magic revision, we turn it into either its + * physical branch equivalent (if one exists) or into + * its base revision, which we assume exists. + */ + dots = numdots (tag); + if (dots > 2 && (dots & 1) != 0) + { + branch = strrchr (tag, '.'); + cp = branch++ - 1; + while (*cp != '.') + cp--; + + /* see if we have .magic-branch. (".0.") */ + magic = xmalloc (strlen (tag) + 1); + (void) sprintf (magic, ".%d.", RCS_MAGIC_BRANCH); + if (strncmp (magic, cp, strlen (magic)) == 0) + { + /* it's magic. See if the branch exists */ + *cp = '\0'; /* turn it into a revision */ + (void) sprintf (magic, "%s.%s", tag, branch); + branch = RCS_getbranch (rcs, magic, 1); + free (magic); + if (branch != NULL) + { + free (tag); + return (branch); + } + return (tag); + } + free (magic); + } + } + else + { + /* The tag wasn't there, so return the head or NULL */ + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + } + + /* + * numeric tag processing: + * 1) revision number - just return it + * 2) branch number - find head of branch + */ + + /* strip trailing dots */ + while (tag[strlen (tag) - 1] == '.') + tag[strlen (tag) - 1] = '\0'; + + if ((numdots (tag) & 1) == 0) + { + char *branch; + + /* we have a branch tag, so we need to walk the branch */ + branch = RCS_getbranch (rcs, tag, force_tag_match); + if (tag_allocated) + free (tag); + return branch; + } + else + { + Node *p; + + /* we have a revision tag, so make sure it exists */ + p = findnode (rcs->versions, tag); + if (p != NULL) + { + /* We have found a numeric revision for the revision tag. + To support expanding the RCS keyword Name, if + SIMPLE_TAG is not NULL, tell the the caller that this + is a simple tag which co will recognize. FIXME: Are + there other cases in which we should set this? In + particular, what if we expand RCS keywords internally + without calling co? */ + if (simple_tag != NULL) + *simple_tag = 1; + if (! tag_allocated) + tag = xstrdup (tag); + return (tag); + } + else + { + /* The revision wasn't there, so return the head or NULL */ + if (tag_allocated) + free (tag); + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + } +} + +/* + * Return a "magic" revision as a virtual branch off of REV for the RCS file. + * A "magic" revision is one which is unique in the RCS file. By unique, I + * mean we return a revision which: + * - has a branch of 0 (see rcs.h RCS_MAGIC_BRANCH) + * - has a revision component which is not an existing branch off REV + * - has a revision component which is not an existing magic revision + * - is an even-numbered revision, to avoid conflicts with vendor branches + * The first point is what makes it "magic". + * + * As an example, if we pass in 1.37 as REV, we will look for an existing + * branch called 1.37.2. If it did not exist, we would look for an + * existing symbolic tag with a numeric part equal to 1.37.0.2. If that + * didn't exist, then we know that the 1.37.2 branch can be reserved by + * creating a symbolic tag with 1.37.0.2 as the numeric part. + * + * This allows us to fork development with very little overhead -- just a + * symbolic tag is used in the RCS file. When a commit is done, a physical + * branch is dynamically created to hold the new revision. + * + * Note: We assume that REV is an RCS revision and not a branch number. + */ +static char *check_rev; +char * +RCS_magicrev (rcs, rev) + RCSNode *rcs; + char *rev; +{ + int rev_num; + char *xrev, *test_branch; + + xrev = xmalloc (strlen (rev) + 14); /* enough for .0.number */ + check_rev = xrev; + + /* only look at even numbered branches */ + for (rev_num = 2; ; rev_num += 2) + { + /* see if the physical branch exists */ + (void) sprintf (xrev, "%s.%d", rev, rev_num); + test_branch = RCS_getbranch (rcs, xrev, 1); + if (test_branch != NULL) /* it did, so keep looking */ + { + free (test_branch); + continue; + } + + /* now, create a "magic" revision */ + (void) sprintf (xrev, "%s.%d.%d", rev, RCS_MAGIC_BRANCH, rev_num); + + /* walk the symbols list to see if a magic one already exists */ + if (walklist (RCS_symbols(rcs), checkmagic_proc, NULL) != 0) + continue; + + /* we found a free magic branch. Claim it as ours */ + return (xrev); + } +} + +/* + * walklist proc to look for a match in the symbols list. + * Returns 0 if the symbol does not match, 1 if it does. + */ +static int +checkmagic_proc (p, closure) + Node *p; + void *closure; +{ + if (STREQ (check_rev, p->data)) + return (1); + else + return (0); +} + +/* + * Given an RCSNode, returns non-zero if the specified revision number + * or symbolic tag resolves to a "branch" within the rcs file. + * + * FIXME: this is the same as RCS_nodeisbranch except for the special + * case for handling a null rcsnode. + */ +int +RCS_isbranch (rcs, rev) + RCSNode *rcs; + const char *rev; +{ + /* numeric revisions are easy -- even number of dots is a branch */ + if (isdigit ((unsigned char) *rev)) + return ((numdots (rev) & 1) == 0); + + /* assume a revision if you can't find the RCS info */ + if (rcs == NULL) + return (0); + + /* now, look for a match in the symbols list */ + return (RCS_nodeisbranch (rcs, rev)); +} + +/* + * Given an RCSNode, returns non-zero if the specified revision number + * or symbolic tag resolves to a "branch" within the rcs file. We do + * take into account any magic branches as well. + */ +int +RCS_nodeisbranch (rcs, rev) + RCSNode *rcs; + const char *rev; +{ + int dots; + char *version; + + assert (rcs != NULL); + + /* numeric revisions are easy -- even number of dots is a branch */ + if (isdigit ((unsigned char) *rev)) + return ((numdots (rev) & 1) == 0); + + version = translate_symtag (rcs, rev); + if (version == NULL) + return (0); + dots = numdots (version); + if ((dots & 1) == 0) + { + free (version); + return (1); + } + + /* got a symbolic tag match, but it's not a branch; see if it's magic */ + if (dots > 2) + { + char *magic; + char *branch = strrchr (version, '.'); + char *cp = branch - 1; + while (*cp != '.') + cp--; + + /* see if we have .magic-branch. (".0.") */ + magic = xmalloc (strlen (version) + 1); + (void) sprintf (magic, ".%d.", RCS_MAGIC_BRANCH); + if (strncmp (magic, cp, strlen (magic)) == 0) + { + free (magic); + free (version); + return (1); + } + free (magic); + free (version); + } + return (0); +} + +/* + * Returns a pointer to malloc'ed memory which contains the branch + * for the specified *symbolic* tag. Magic branches are handled correctly. + */ +char * +RCS_whatbranch (rcs, rev) + RCSNode *rcs; + const char *rev; +{ + char *version; + int dots; + + /* assume no branch if you can't find the RCS info */ + if (rcs == NULL) + return ((char *) NULL); + + /* now, look for a match in the symbols list */ + version = translate_symtag (rcs, rev); + if (version == NULL) + return ((char *) NULL); + dots = numdots (version); + if ((dots & 1) == 0) + return (version); + + /* got a symbolic tag match, but it's not a branch; see if it's magic */ + if (dots > 2) + { + char *magic; + char *branch = strrchr (version, '.'); + char *cp = branch++ - 1; + while (*cp != '.') + cp--; + + /* see if we have .magic-branch. (".0.") */ + magic = xmalloc (strlen (version) + 1); + (void) sprintf (magic, ".%d.", RCS_MAGIC_BRANCH); + if (strncmp (magic, cp, strlen (magic)) == 0) + { + /* yep. it's magic. now, construct the real branch */ + *cp = '\0'; /* turn it into a revision */ + (void) sprintf (magic, "%s.%s", version, branch); + free (version); + return (magic); + } + free (magic); + free (version); + } + return ((char *) NULL); +} + +/* + * Get the head of the specified branch. If the branch does not exist, + * return NULL or RCS_head depending on force_tag_match. + * Returns NULL or a newly malloc'd string. + */ +char * +RCS_getbranch (rcs, tag, force_tag_match) + RCSNode *rcs; + char *tag; + int force_tag_match; +{ + Node *p, *head; + RCSVers *vn; + char *xtag; + char *nextvers; + char *cp; + + /* make sure we have something to look at... */ + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* find out if the tag contains a dot, or is on the trunk */ + cp = strrchr (tag, '.'); + + /* trunk processing is the special case */ + if (cp == NULL) + { + xtag = xmalloc (strlen (tag) + 1 + 1); /* +1 for an extra . */ + (void) strcpy (xtag, tag); + (void) strcat (xtag, "."); + for (cp = rcs->head; cp != NULL;) + { + if (strncmp (xtag, cp, strlen (xtag)) == 0) + break; + p = findnode (rcs->versions, cp); + if (p == NULL) + { + free (xtag); + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + vn = (RCSVers *) p->data; + cp = vn->next; + } + free (xtag); + if (cp == NULL) + { + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + return (xstrdup (cp)); + } + + /* if it had a `.', terminate the string so we have the base revision */ + *cp = '\0'; + + /* look up the revision this branch is based on */ + p = findnode (rcs->versions, tag); + + /* put the . back so we have the branch again */ + *cp = '.'; + + if (p == NULL) + { + /* if the base revision didn't exist, return head or NULL */ + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + + /* find the first element of the branch we are looking for */ + vn = (RCSVers *) p->data; + if (vn->branches == NULL) + return (NULL); + xtag = xmalloc (strlen (tag) + 1 + 1); /* 1 for the extra '.' */ + (void) strcpy (xtag, tag); + (void) strcat (xtag, "."); + head = vn->branches->list; + for (p = head->next; p != head; p = p->next) + if (strncmp (p->key, xtag, strlen (xtag)) == 0) + break; + free (xtag); + + if (p == head) + { + /* we didn't find a match so return head or NULL */ + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + + /* now walk the next pointers of the branch */ + nextvers = p->key; + do + { + p = findnode (rcs->versions, nextvers); + if (p == NULL) + { + /* a link in the chain is missing - return head or NULL */ + if (force_tag_match) + return (NULL); + else + return (RCS_head (rcs)); + } + vn = (RCSVers *) p->data; + nextvers = vn->next; + } while (nextvers != NULL); + + /* we have the version in our hand, so go for it */ + return (xstrdup (vn->version)); +} + +/* Returns the head of the branch which REV is on. REV can be a + branch tag or non-branch tag; symbolic or numeric. + + Returns a newly malloc'd string. Returns NULL if a symbolic name + isn't found. */ + +char * +RCS_branch_head (rcs, rev) + RCSNode *rcs; + char *rev; +{ + char *num; + char *br; + char *retval; + + assert (rcs != NULL); + + if (RCS_nodeisbranch (rcs, rev)) + return RCS_getbranch (rcs, rev, 1); + + if (isdigit ((unsigned char) *rev)) + num = xstrdup (rev); + else + { + num = translate_symtag (rcs, rev); + if (num == NULL) + return NULL; + } + br = truncate_revnum (num); + retval = RCS_getbranch (rcs, br, 1); + free (br); + free (num); + return retval; +} + +/* Get the branch point for a particular branch, that is the first + revision on that branch. For example, RCS_getbranchpoint (rcs, + "1.3.2") will normally return "1.3.2.1". TARGET may be either a + branch number or a revision number; if a revnum, find the + branchpoint of the branch to which TARGET belongs. + + Return RCS_head if TARGET is on the trunk or if the root node could + not be found (this is sort of backwards from our behavior on a branch; + the rationale is that the return value is a revision from which you + can start walking the next fields and end up at TARGET). + Return NULL on error. */ + +static char * +RCS_getbranchpoint (rcs, target) + RCSNode *rcs; + char *target; +{ + char *branch, *bp; + Node *vp; + RCSVers *rev; + int dots, isrevnum, brlen; + + dots = numdots (target); + isrevnum = dots & 1; + + if (dots == 1) + /* TARGET is a trunk revision; return rcs->head. */ + return (RCS_head (rcs)); + + /* Get the revision number of the node at which TARGET's branch is + rooted. If TARGET is a branch number, lop off the last field; + if it's a revision number, lop off the last *two* fields. */ + branch = xstrdup (target); + bp = strrchr (branch, '.'); + if (bp == NULL) + error (1, 0, "%s: confused revision number %s", + rcs->path, target); + if (isrevnum) + while (*--bp != '.') + ; + *bp = '\0'; + + vp = findnode (rcs->versions, branch); + if (vp == NULL) + { + error (0, 0, "%s: can't find branch point %s", rcs->path, target); + return NULL; + } + rev = (RCSVers *) vp->data; + + *bp++ = '.'; + while (*bp && *bp != '.') + ++bp; + brlen = bp - branch; + + vp = rev->branches->list->next; + while (vp != rev->branches->list) + { + /* BRANCH may be a genuine branch number, e.g. `1.1.3', or + maybe a full revision number, e.g. `1.1.3.6'. We have + found our branch point if the first BRANCHLEN characters + of the revision number match, *and* if the following + character is a dot. */ + if (strncmp (vp->key, branch, brlen) == 0 && vp->key[brlen] == '.') + break; + vp = vp->next; + } + + free (branch); + if (vp == rev->branches->list) + { + error (0, 0, "%s: can't find branch point %s", rcs->path, target); + return NULL; + } + else + return (xstrdup (vp->key)); +} + +/* + * Get the head of the RCS file. If branch is set, this is the head of the + * branch, otherwise the real head. + * Returns NULL or a newly malloc'd string. + */ +char * +RCS_head (rcs) + RCSNode *rcs; +{ + /* make sure we have something to look at... */ + assert (rcs != NULL); + + /* + * NOTE: we call getbranch with force_tag_match set to avoid any + * possibility of recursion + */ + if (rcs->branch) + return (RCS_getbranch (rcs, rcs->branch, 1)); + else + return (xstrdup (rcs->head)); +} + +/* + * Get the most recent revision, based on the supplied date, but use some + * funky stuff and follow the vendor branch maybe + */ +char * +RCS_getdate (rcs, date, force_tag_match) + RCSNode *rcs; + char *date; + int force_tag_match; +{ + char *cur_rev = NULL; + char *retval = NULL; + Node *p; + RCSVers *vers = NULL; + + /* make sure we have something to look at... */ + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* if the head is on a branch, try the branch first */ + if (rcs->branch != NULL) + retval = RCS_getdatebranch (rcs, date, rcs->branch); + + /* if we found a match, we are done */ + if (retval != NULL) + return (retval); + + /* otherwise if we have a trunk, try it */ + if (rcs->head) + { + p = findnode (rcs->versions, rcs->head); + while (p != NULL) + { + /* if the date of this one is before date, take it */ + vers = (RCSVers *) p->data; + if (RCS_datecmp (vers->date, date) <= 0) + { + cur_rev = vers->version; + break; + } + + /* if there is a next version, find the node */ + if (vers->next != NULL) + p = findnode (rcs->versions, vers->next); + else + p = (Node *) NULL; + } + } + + /* + * at this point, either we have the revision we want, or we have the + * first revision on the trunk (1.1?) in our hands + */ + + /* if we found what we're looking for, and it's not 1.1 return it */ + if (cur_rev != NULL) + { + if (! STREQ (cur_rev, "1.1")) + return (xstrdup (cur_rev)); + + /* This is 1.1; if the date of 1.1 is not the same as that for the + 1.1.1.1 version, then return 1.1. This happens when the first + version of a file is created by a regular cvs add and commit, + and there is a subsequent cvs import of the same file. */ + p = findnode (rcs->versions, "1.1.1.1"); + if (p) + { + vers = (RCSVers *) p->data; + if (RCS_datecmp (vers->date, date) != 0) + return xstrdup ("1.1"); + } + } + + /* look on the vendor branch */ + retval = RCS_getdatebranch (rcs, date, CVSBRANCH); + + /* + * if we found a match, return it; otherwise, we return the first + * revision on the trunk or NULL depending on force_tag_match and the + * date of the first rev + */ + if (retval != NULL) + return (retval); + + if (!force_tag_match || RCS_datecmp (vers->date, date) <= 0) + return (xstrdup (vers->version)); + else + return (NULL); +} + +/* + * Look up the last element on a branch that was put in before the specified + * date (return the rev or NULL) + */ +static char * +RCS_getdatebranch (rcs, date, branch) + RCSNode *rcs; + char *date; + char *branch; +{ + char *cur_rev = NULL; + char *cp; + char *xbranch, *xrev; + Node *p; + RCSVers *vers; + + /* look up the first revision on the branch */ + xrev = xstrdup (branch); + cp = strrchr (xrev, '.'); + if (cp == NULL) + { + free (xrev); + return (NULL); + } + *cp = '\0'; /* turn it into a revision */ + + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + p = findnode (rcs->versions, xrev); + free (xrev); + if (p == NULL) + return (NULL); + vers = (RCSVers *) p->data; + + /* Tentatively use this revision, if it is early enough. */ + if (RCS_datecmp (vers->date, date) <= 0) + cur_rev = vers->version; + + /* If no branches list, return now. This is what happens if the branch + is a (magic) branch with no revisions yet. */ + if (vers->branches == NULL) + return xstrdup (cur_rev); + + /* walk the branches list looking for the branch number */ + xbranch = xmalloc (strlen (branch) + 1 + 1); /* +1 for the extra dot */ + (void) strcpy (xbranch, branch); + (void) strcat (xbranch, "."); + for (p = vers->branches->list->next; p != vers->branches->list; p = p->next) + if (strncmp (p->key, xbranch, strlen (xbranch)) == 0) + break; + free (xbranch); + if (p == vers->branches->list) + { + /* This is what happens if the branch is a (magic) branch with + no revisions yet. Similar to the case where vers->branches == + NULL, except here there was a another branch off the same + branchpoint. */ + return xstrdup (cur_rev); + } + + p = findnode (rcs->versions, p->key); + + /* walk the next pointers until you find the end, or the date is too late */ + while (p != NULL) + { + vers = (RCSVers *) p->data; + if (RCS_datecmp (vers->date, date) <= 0) + cur_rev = vers->version; + else + break; + + /* if there is a next version, find the node */ + if (vers->next != NULL) + p = findnode (rcs->versions, vers->next); + else + p = (Node *) NULL; + } + + /* Return whatever we found, which may be NULL. */ + return xstrdup (cur_rev); +} + +/* + * Compare two dates in RCS format. Beware the change in format on January 1, + * 2000, when years go from 2-digit to full format. + */ +int +RCS_datecmp (date1, date2) + char *date1, *date2; +{ + int length_diff = strlen (date1) - strlen (date2); + + return (length_diff ? length_diff : strcmp (date1, date2)); +} + +/* Look up revision REV in RCS and return the date specified for the + revision minus FUDGE seconds (FUDGE will generally be one, so that the + logically previous revision will be found later, or zero, if we want + the exact date). + + The return value is the date being returned as a time_t, or (time_t)-1 + on error (previously was documented as zero on error; I haven't checked + the callers to make sure that they really check for (time_t)-1, but + the latter is what this function really returns). If DATE is non-NULL, + then it must point to MAXDATELEN characters, and we store the same + return value there in DATEFORM format. */ +time_t +RCS_getrevtime (rcs, rev, date, fudge) + RCSNode *rcs; + char *rev; + char *date; + int fudge; +{ + char tdate[MAXDATELEN]; + struct tm xtm, *ftm; + time_t revdate = 0; + Node *p; + RCSVers *vers; + + /* make sure we have something to look at... */ + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* look up the revision */ + p = findnode (rcs->versions, rev); + if (p == NULL) + return (-1); + vers = (RCSVers *) p->data; + + /* split up the date */ + ftm = &xtm; + (void) sscanf (vers->date, SDATEFORM, &ftm->tm_year, &ftm->tm_mon, + &ftm->tm_mday, &ftm->tm_hour, &ftm->tm_min, + &ftm->tm_sec); + + /* If the year is from 1900 to 1999, RCS files contain only two + digits, and sscanf gives us a year from 0-99. If the year is + 2000+, RCS files contain all four digits and we subtract 1900, + because the tm_year field should contain years since 1900. */ + + if (ftm->tm_year > 1900) + ftm->tm_year -= 1900; + + /* put the date in a form getdate can grok */ + (void) sprintf (tdate, "%d/%d/%d GMT %d:%d:%d", ftm->tm_mon, + ftm->tm_mday, ftm->tm_year + 1900, ftm->tm_hour, + ftm->tm_min, ftm->tm_sec); + + /* turn it into seconds since the epoch */ + revdate = get_date (tdate, (struct timeb *) NULL); + if (revdate != (time_t) -1) + { + revdate -= fudge; /* remove "fudge" seconds */ + if (date) + { + /* put an appropriate string into ``date'' if we were given one */ + ftm = gmtime (&revdate); + (void) sprintf (date, DATEFORM, + ftm->tm_year + (ftm->tm_year < 100 ? 0 : 1900), + ftm->tm_mon + 1, ftm->tm_mday, ftm->tm_hour, + ftm->tm_min, ftm->tm_sec); + } + } + return (revdate); +} + +List * +RCS_getlocks (rcs) + RCSNode *rcs; +{ + assert(rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rcs->locks_data) { + rcs->locks = getlist (); + do_locks (rcs->locks, rcs->locks_data); + free(rcs->locks_data); + rcs->locks_data = NULL; + } + + return rcs->locks; +} + +List * +RCS_symbols(rcs) + RCSNode *rcs; +{ + assert(rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rcs->symbols_data) { + rcs->symbols = getlist (); + do_symbols (rcs->symbols, rcs->symbols_data); + free(rcs->symbols_data); + rcs->symbols_data = NULL; + } + + return rcs->symbols; +} + +/* + * Return the version associated with a particular symbolic tag. + * Returns NULL or a newly malloc'd string. + */ +static char * +translate_symtag (rcs, tag) + RCSNode *rcs; + const char *tag; +{ + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rcs->symbols != NULL) + { + Node *p; + + /* The symbols have already been converted into a list. */ + p = findnode (rcs->symbols, tag); + if (p == NULL) + return NULL; + + return xstrdup (p->data); + } + + if (rcs->symbols_data != NULL) + { + size_t len; + char *cp; + + /* Look through the RCS symbols information. This is like + do_symbols, but we don't add the information to a list. In + most cases, we will only be called once for this file, so + generating the list is unnecessary overhead. */ + + len = strlen (tag); + cp = rcs->symbols_data; + while ((cp = strchr (cp, tag[0])) != NULL) + { + if ((cp == rcs->symbols_data || whitespace (cp[-1])) + && strncmp (cp, tag, len) == 0 + && cp[len] == ':') + { + char *v, *r; + + /* We found the tag. Return the version number. */ + + cp += len + 1; + v = cp; + while (! whitespace (*cp) && *cp != '\0') + ++cp; + r = xmalloc (cp - v + 1); + strncpy (r, v, cp - v); + r[cp - v] = '\0'; + return r; + } + + while (! whitespace (*cp) && *cp != '\0') + ++cp; + } + } + + return NULL; +} + +/* + * The argument ARG is the getopt remainder of the -k option specified on the + * command line. This function returns malloc'ed space that can be used + * directly in calls to RCS V5, with the -k flag munged correctly. + */ +char * +RCS_check_kflag (arg) + const char *arg; +{ + static const char *const keyword_usage[] = + { + "%s %s: invalid RCS keyword expansion mode\n", + "Valid expansion modes include:\n", + " -kkv\tGenerate keywords using the default form.\n", + " -kkvl\tLike -kkv, except locker's name inserted.\n", + " -kk\tGenerate only keyword names in keyword strings.\n", + " -kv\tGenerate only keyword values in keyword strings.\n", + " -ko\tGenerate the old keyword string (no changes from checked in file).\n", + " -kb\tGenerate binary file unmodified (merges not allowed) (RCS 5.7).\n", + "(Specify the --help global option for a list of other help options)\n", + NULL, + }; + /* Big enough to hold any of the strings from kflags. */ + char karg[10]; + char const *const *cpp = NULL; + + if (arg) + { + for (cpp = kflags; *cpp != NULL; cpp++) + { + if (STREQ (arg, *cpp)) + break; + } + } + + if (arg == NULL || *cpp == NULL) + { + usage (keyword_usage); + } + + (void) sprintf (karg, "-k%s", *cpp); + return (xstrdup (karg)); +} + +/* + * Do some consistency checks on the symbolic tag... These should equate + * pretty close to what RCS checks, though I don't know for certain. + */ +void +RCS_check_tag (tag) + const char *tag; +{ + char *invalid = "$,.:;@"; /* invalid RCS tag characters */ + const char *cp; + + /* + * The first character must be an alphabetic letter. The remaining + * characters cannot be non-visible graphic characters, and must not be + * in the set of "invalid" RCS identifier characters. + */ + if (isalpha ((unsigned char) *tag)) + { + for (cp = tag; *cp; cp++) + { + if (!isgraph ((unsigned char) *cp)) + error (1, 0, "tag `%s' has non-visible graphic characters", + tag); + if (strchr (invalid, *cp)) + error (1, 0, "tag `%s' must not contain the characters `%s'", + tag, invalid); + } + } + else + error (1, 0, "tag `%s' must start with a letter", tag); +} + +/* + * TRUE if argument has valid syntax for an RCS revision or + * branch number. All characters must be digits or dots, first + * and last characters must be digits, and no two consecutive + * characters may be dots. + * + * Intended for classifying things, so this function doesn't + * call error. + */ +int +RCS_valid_rev (rev) + char *rev; +{ + char last, c; + last = *rev++; + if (!isdigit ((unsigned char) last)) + return 0; + while ((c = *rev++)) /* Extra parens placate -Wall gcc option */ + { + if (c == '.') + { + if (last == '.') + return 0; + continue; + } + last = c; + if (!isdigit ((unsigned char) c)) + return 0; + } + if (!isdigit ((unsigned char) last)) + return 0; + return 1; +} + +/* + * Return true if RCS revision with TAG is a dead revision. + */ +int +RCS_isdead (rcs, tag) + RCSNode *rcs; + const char *tag; +{ + Node *p; + RCSVers *version; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + p = findnode (rcs->versions, tag); + if (p == NULL) + return (0); + + version = (RCSVers *) p->data; + return (version->dead); +} + +/* Return the RCS keyword expansion mode. For example "b" for binary. + Returns a pointer into storage which is allocated and freed along with + the rest of the RCS information; the caller should not modify this + storage. Returns NULL if the RCS file does not specify a keyword + expansion mode; for all other errors, die with a fatal error. */ +char * +RCS_getexpand (rcs) + RCSNode *rcs; +{ + /* Since RCS_parsercsfile_i now reads expand, don't need to worry + about RCS_reparsercsfile. */ + assert (rcs != NULL); + return rcs->expand; +} + +/* Set keyword expansion mode to EXPAND. For example "b" for binary. */ +void +RCS_setexpand (rcs, expand) + RCSNode *rcs; + char *expand; +{ + /* Since RCS_parsercsfile_i now reads expand, don't need to worry + about RCS_reparsercsfile. */ + assert (rcs != NULL); + if (rcs->expand != NULL) + free (rcs->expand); + rcs->expand = xstrdup (expand); +} + +/* RCS keywords, and a matching enum. */ +struct rcs_keyword +{ + const char *string; + size_t len; +}; +#define KEYWORD_INIT(s) (s), sizeof (s) - 1 +static const struct rcs_keyword keywords[] = +{ + { KEYWORD_INIT ("Author") }, + { KEYWORD_INIT ("Date") }, + { KEYWORD_INIT ("Header") }, + { KEYWORD_INIT ("Id") }, + { KEYWORD_INIT ("Locker") }, + { KEYWORD_INIT ("Log") }, + { KEYWORD_INIT ("Name") }, + { KEYWORD_INIT ("RCSfile") }, + { KEYWORD_INIT ("Revision") }, + { KEYWORD_INIT ("Source") }, + { KEYWORD_INIT ("State") }, + { NULL, 0 } +}; +enum keyword +{ + KEYWORD_AUTHOR = 0, + KEYWORD_DATE, + KEYWORD_HEADER, + KEYWORD_ID, + KEYWORD_LOCKER, + KEYWORD_LOG, + KEYWORD_NAME, + KEYWORD_RCSFILE, + KEYWORD_REVISION, + KEYWORD_SOURCE, + KEYWORD_STATE +}; + +/* Convert an RCS date string into a readable string. This is like + the RCS date2str function. */ + +static char * +printable_date (rcs_date) + const char *rcs_date; +{ + int year, mon, mday, hour, min, sec; + char buf[100]; + + (void) sscanf (rcs_date, SDATEFORM, &year, &mon, &mday, &hour, &min, + &sec); + if (year < 1900) + year += 1900; + sprintf (buf, "%04d/%02d/%02d %02d:%02d:%02d", year, mon, mday, + hour, min, sec); + return xstrdup (buf); +} + +/* Escape the characters in a string so that it can be included in an + RCS value. */ + +static char * +escape_keyword_value (value, free_value) + const char *value; + int *free_value; +{ + char *ret, *t; + const char *s; + + for (s = value; *s != '\0'; s++) + { + char c; + + c = *s; + if (c == '\t' + || c == '\n' + || c == '\\' + || c == ' ' + || c == '$') + { + break; + } + } + + if (*s == '\0') + { + *free_value = 0; + return (char *) value; + } + + ret = xmalloc (strlen (value) * 4 + 1); + *free_value = 1; + + for (s = value, t = ret; *s != '\0'; s++, t++) + { + switch (*s) + { + default: + *t = *s; + break; + case '\t': + *t++ = '\\'; + *t = 't'; + break; + case '\n': + *t++ = '\\'; + *t = 'n'; + break; + case '\\': + *t++ = '\\'; + *t = '\\'; + break; + case ' ': + *t++ = '\\'; + *t++ = '0'; + *t++ = '4'; + *t = '0'; + break; + case '$': + *t++ = '\\'; + *t++ = '0'; + *t++ = '4'; + *t = '4'; + break; + } + } + + *t = '\0'; + + return ret; +} + +/* Expand RCS keywords in the memory buffer BUF of length LEN. This + applies to file RCS and version VERS. If NAME is not NULL, and is + not a numeric revision, then it is the symbolic tag used for the + checkout. EXPAND indicates how to expand the keywords. This + function sets *RETBUF and *RETLEN to the new buffer and length. + This function may modify the buffer BUF. If BUF != *RETBUF, then + RETBUF is a newly allocated buffer. */ + +static void +expand_keywords (rcs, ver, name, log, loglen, expand, buf, len, retbuf, retlen) + RCSNode *rcs; + RCSVers *ver; + const char *name; + const char *log; + size_t loglen; + enum kflag expand; + char *buf; + size_t len; + char **retbuf; + size_t *retlen; +{ + struct expand_buffer + { + struct expand_buffer *next; + char *data; + size_t len; + int free_data; + } *ebufs = NULL; + struct expand_buffer *ebuf_last = NULL; + size_t ebuf_len = 0; + char *locker; + char *srch, *srch_next; + size_t srch_len; + + if (expand == KFLAG_O || expand == KFLAG_B) + { + *retbuf = buf; + *retlen = len; + return; + } + + /* If we are using -kkvl, dig out the locker information if any. */ + locker = NULL; + if (expand == KFLAG_KVL) + { + Node *lock; + lock = findnode (RCS_getlocks(rcs), ver->version); + if (lock != NULL) + locker = xstrdup (lock->data); + } + + /* RCS keywords look like $STRING$ or $STRING: VALUE$. */ + srch = buf; + srch_len = len; + while ((srch_next = memchr (srch, '$', srch_len)) != NULL) + { + char *s, *send; + size_t slen; + const struct rcs_keyword *keyword; + enum keyword kw; + char *value; + int free_value; + char *sub; + size_t sublen; + + srch_len -= (srch_next + 1) - srch; + srch = srch_next + 1; + + /* Look for the first non alphabetic character after the '$'. */ + send = srch + srch_len; + for (s = srch; s < send; s++) + if (! isalpha ((unsigned char) *s)) + break; + + /* If the first non alphabetic character is not '$' or ':', + then this is not an RCS keyword. */ + if (s == send || (*s != '$' && *s != ':')) + continue; + + /* See if this is one of the keywords. */ + slen = s - srch; + for (keyword = keywords; keyword->string != NULL; keyword++) + { + if (keyword->len == slen + && strncmp (keyword->string, srch, slen) == 0) + { + break; + } + } + if (keyword->string == NULL) + continue; + + kw = (enum keyword) (keyword - keywords); + + /* If the keyword ends with a ':', then the old value consists + of the characters up to the next '$'. If there is no '$' + before the end of the line, though, then this wasn't an RCS + keyword after all. */ + if (*s == ':') + { + for (; s < send; s++) + if (*s == '$' || *s == '\n') + break; + if (s == send || *s != '$') + continue; + } + + /* At this point we must replace the string from SRCH to S + with the expansion of the keyword KW. */ + + /* Get the value to use. */ + free_value = 0; + if (expand == KFLAG_K) + value = NULL; + else + { + switch (kw) + { + default: + abort (); + + case KEYWORD_AUTHOR: + value = ver->author; + break; + + case KEYWORD_DATE: + value = printable_date (ver->date); + free_value = 1; + break; + + case KEYWORD_HEADER: + case KEYWORD_ID: + { + char *path; + int free_path; + char *date; + + if (kw == KEYWORD_HEADER) + path = rcs->path; + else + path = last_component (rcs->path); + path = escape_keyword_value (path, &free_path); + date = printable_date (ver->date); + value = xmalloc (strlen (path) + + strlen (ver->version) + + strlen (date) + + strlen (ver->author) + + strlen (ver->state) + + (locker == NULL ? 0 : strlen (locker)) + + 20); + + sprintf (value, "%s %s %s %s %s%s%s", + path, ver->version, date, ver->author, + ver->state, + locker != NULL ? " " : "", + locker != NULL ? locker : ""); + if (free_path) + free (path); + free (date); + free_value = 1; + } + break; + + case KEYWORD_LOCKER: + value = locker; + break; + + case KEYWORD_LOG: + case KEYWORD_RCSFILE: + value = escape_keyword_value (last_component (rcs->path), + &free_value); + break; + + case KEYWORD_NAME: + if (name != NULL && ! isdigit ((unsigned char) *name)) + value = (char *) name; + else + value = NULL; + break; + + case KEYWORD_REVISION: + value = ver->version; + break; + + case KEYWORD_SOURCE: + value = escape_keyword_value (rcs->path, &free_value); + break; + + case KEYWORD_STATE: + value = ver->state; + break; + } + } + + sub = xmalloc (keyword->len + + (value == NULL ? 0 : strlen (value)) + + 10); + if (expand == KFLAG_V) + { + /* Decrement SRCH and increment S to remove the $ + characters. */ + --srch; + ++srch_len; + ++s; + sublen = 0; + } + else + { + strcpy (sub, keyword->string); + sublen = strlen (keyword->string); + if (expand != KFLAG_K) + { + sub[sublen] = ':'; + sub[sublen + 1] = ' '; + sublen += 2; + } + } + if (value != NULL) + { + strcpy (sub + sublen, value); + sublen += strlen (value); + } + if (expand != KFLAG_V && expand != KFLAG_K) + { + sub[sublen] = ' '; + ++sublen; + sub[sublen] = '\0'; + } + + if (free_value) + free (value); + + /* The Log keyword requires special handling. This behaviour + is taken from RCS 5.7. The special log message is what RCS + uses for ci -k. */ + if (kw == KEYWORD_LOG + && (sizeof "checked in with -k by " <= loglen + || strncmp (log, "checked in with -k by ", + sizeof "checked in with -k by " - 1) != 0)) + { + char *start; + char *leader; + size_t leader_len, leader_sp_len; + const char *logend; + const char *snl; + int cnl; + char *date; + const char *sl; + + /* We are going to insert the trailing $ ourselves, before + the log message, so we must remove it from S, if we + haven't done so already. */ + if (expand != KFLAG_V) + ++s; + + /* Find the start of the line. */ + start = srch; + while (start > buf && start[-1] != '\n') + --start; + + /* Copy the start of the line to use as a comment leader. */ + leader_len = srch - start; + if (expand != KFLAG_V) + --leader_len; + leader = xmalloc (leader_len); + memcpy (leader, start, leader_len); + leader_sp_len = leader_len; + while (leader_sp_len > 0 && leader[leader_sp_len - 1] == ' ') + --leader_sp_len; + + /* RCS does some checking for an old style of Log here, + but we don't bother. RCS issues a warning if it + changes anything. */ + + /* Count the number of newlines in the log message so that + we know how many copies of the leader we will need. */ + cnl = 0; + logend = log + loglen; + for (snl = log; snl < logend; snl++) + if (*snl == '\n') + ++cnl; + + date = printable_date (ver->date); + sub = xrealloc (sub, + (sublen + + sizeof "Revision" + + strlen (ver->version) + + strlen (date) + + strlen (ver->author) + + loglen + + (cnl + 2) * leader_len + + 20)); + if (expand != KFLAG_V) + { + sub[sublen] = '$'; + ++sublen; + } + sub[sublen] = '\n'; + ++sublen; + memcpy (sub + sublen, leader, leader_len); + sublen += leader_len; + sprintf (sub + sublen, "Revision %s %s %s\n", + ver->version, date, ver->author); + sublen += strlen (sub + sublen); + free (date); + + sl = log; + while (sl < logend) + { + if (*sl == '\n') + { + memcpy (sub + sublen, leader, leader_sp_len); + sublen += leader_sp_len; + sub[sublen] = '\n'; + ++sublen; + ++sl; + } + else + { + const char *slnl; + + memcpy (sub + sublen, leader, leader_len); + sublen += leader_len; + for (slnl = sl; slnl < logend && *slnl != '\n'; ++slnl) + ; + if (slnl < logend) + ++slnl; + memcpy (sub + sublen, sl, slnl - sl); + sublen += slnl - sl; + sl = slnl; + } + } + + memcpy (sub + sublen, leader, leader_sp_len); + sublen += leader_sp_len; + + free (leader); + } + + /* Now SUB contains a string which is to replace the string + from SRCH to S. SUBLEN is the length of SUB. */ + + if (srch + sublen == s) + { + memcpy (srch, sub, sublen); + free (sub); + } + else + { + struct expand_buffer *ebuf; + + /* We need to change the size of the buffer. We build a + list of expand_buffer structures. Each expand_buffer + structure represents a portion of the final output. We + concatenate them back into a single buffer when we are + done. This minimizes the number of potentially large + buffer copies we must do. */ + + if (ebufs == NULL) + { + ebufs = (struct expand_buffer *) xmalloc (sizeof *ebuf); + ebufs->next = NULL; + ebufs->data = buf; + ebufs->free_data = 0; + ebuf_len = srch - buf; + ebufs->len = ebuf_len; + ebuf_last = ebufs; + } + else + { + assert (srch >= ebuf_last->data); + assert (srch <= ebuf_last->data + ebuf_last->len); + ebuf_len -= ebuf_last->len - (srch - ebuf_last->data); + ebuf_last->len = srch - ebuf_last->data; + } + + ebuf = (struct expand_buffer *) xmalloc (sizeof *ebuf); + ebuf->data = sub; + ebuf->len = sublen; + ebuf->free_data = 1; + ebuf->next = NULL; + ebuf_last->next = ebuf; + ebuf_last = ebuf; + ebuf_len += sublen; + + ebuf = (struct expand_buffer *) xmalloc (sizeof *ebuf); + ebuf->data = s; + ebuf->len = srch_len - (s - srch); + ebuf->free_data = 0; + ebuf->next = NULL; + ebuf_last->next = ebuf; + ebuf_last = ebuf; + ebuf_len += srch_len - (s - srch); + } + + srch_len -= (s - srch); + srch = s; + } + + if (locker != NULL) + free (locker); + + if (ebufs == NULL) + { + *retbuf = buf; + *retlen = len; + } + else + { + char *ret; + + ret = xmalloc (ebuf_len); + *retbuf = ret; + *retlen = ebuf_len; + while (ebufs != NULL) + { + struct expand_buffer *next; + + memcpy (ret, ebufs->data, ebufs->len); + ret += ebufs->len; + if (ebufs->free_data) + free (ebufs->data); + next = ebufs->next; + free (ebufs); + ebufs = next; + } + } +} + +/* Check out a revision from an RCS file. + + If PFN is not NULL, then ignore WORKFILE and SOUT. Call PFN zero + or more times with the contents of the file. CALLERDAT is passed, + uninterpreted, to PFN. (The current code will always call PFN + exactly once for a non empty file; however, the current code + assumes that it can hold the entire file contents in memory, which + is not a good assumption, and might change in the future). + + Otherwise, if WORKFILE is not NULL, check out the revision to + WORKFILE. However, if WORKFILE is not NULL, and noexec is set, + then don't do anything. + + Otherwise, if WORKFILE is NULL, check out the revision to SOUT. If + SOUT is RUN_TTY, then write the contents of the revision to + standard output. When using SOUT, the output is generally a + temporary file; don't bother to get the file modes correct. + + REV is the numeric revision to check out. It may be NULL, which + means to check out the head of the default branch. + + If NAMETAG is not NULL, and is not a numeric revision, then it is + the tag that should be used when expanding the RCS Name keyword. + + OPTIONS is a string such as "-kb" or "-kv" for keyword expansion + options. It may be NULL to use the default expansion mode of the + file, typically "-kkv". + + On an error which prevented checking out the file, either print a + nonfatal error and return 1, or give a fatal error. On success, + return 0. */ + +/* This function mimics the behavior of `rcs co' almost exactly. The + chief difference is in its support for preserving file ownership, + permissions, and special files across checkin and checkout -- see + comments in RCS_checkin for some issues about this. -twp */ + +int +RCS_checkout (rcs, workfile, rev, nametag, options, sout, pfn, callerdat) + RCSNode *rcs; + char *workfile; + char *rev; + char *nametag; + char *options; + char *sout; + RCSCHECKOUTPROC pfn; + void *callerdat; +{ + int free_rev = 0; + enum kflag expand; + FILE *fp, *ofp; + struct stat sb; + struct rcsbuffer rcsbuf; + char *key; + char *value; + size_t len; + int free_value = 0; + char *log = NULL; + size_t loglen; + Node *vp = NULL; +#ifdef PRESERVE_PERMISSIONS_SUPPORT + uid_t rcs_owner = (uid_t) -1; + gid_t rcs_group = (gid_t) -1; + mode_t rcs_mode; + int change_rcs_owner_or_group = 0; + int change_rcs_mode = 0; + int special_file = 0; + unsigned long devnum_long; + dev_t devnum = 0; +#endif + + if (trace) + { + (void) fprintf (stderr, "%s-> checkout (%s, %s, %s, %s)\n", +#ifdef SERVER_SUPPORT + server_active ? "S" : " ", +#else + "", +#endif + rcs->path, + rev != NULL ? rev : "", + options != NULL ? options : "", + (pfn != NULL ? "(function)" + : (workfile != NULL + ? workfile + : (sout != RUN_TTY ? sout : "(stdout)")))); + } + + assert (rev == NULL || isdigit ((unsigned char) *rev)); + + if (noexec && workfile != NULL) + return 0; + + assert (sout == RUN_TTY || workfile == NULL); + assert (pfn == NULL || (sout == RUN_TTY && workfile == NULL)); + + /* Some callers, such as Checkin or remove_file, will pass us a + branch. */ + if (rev != NULL && (numdots (rev) & 1) == 0) + { + rev = RCS_getbranch (rcs, rev, 1); + if (rev == NULL) + error (1, 0, "internal error: bad branch tag in checkout"); + free_rev = 1; + } + + if (rev == NULL || STREQ (rev, rcs->head)) + { + int gothead; + + /* We want the head revision. Try to read it directly. */ + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, &fp, &rcsbuf); + else + rcsbuf_cache_open (rcs, rcs->delta_pos, &fp, &rcsbuf); + + gothead = 0; + if (! rcsbuf_getrevnum (&rcsbuf, &key)) + error (1, 0, "unexpected EOF reading %s", rcs->path); + while (rcsbuf_getkey (&rcsbuf, &key, &value)) + { + if (STREQ (key, "log")) + log = rcsbuf_valcopy (&rcsbuf, value, 0, &loglen); + else if (STREQ (key, "text")) + { + gothead = 1; + break; + } + } + + if (! gothead) + { + error (0, 0, "internal error: cannot find head text"); + if (free_rev) + free (rev); + return 1; + } + + rcsbuf_valpolish (&rcsbuf, value, 0, &len); + + if (fstat (fileno (fp), &sb) < 0) + error (1, errno, "cannot fstat %s", rcs->path); + + rcsbuf_cache (rcs, &rcsbuf); + } + else + { + struct rcsbuffer *rcsbufp; + + /* It isn't the head revision of the trunk. We'll need to + walk through the deltas. */ + + fp = NULL; + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, &fp, &rcsbuf); + + if (fp == NULL) + { + /* If RCS_deltas didn't close the file, we could use fstat + here too. Probably should change it thusly.... */ + if (stat (rcs->path, &sb) < 0) + error (1, errno, "cannot stat %s", rcs->path); + rcsbufp = NULL; + } + else + { + if (fstat (fileno (fp), &sb) < 0) + error (1, errno, "cannot fstat %s", rcs->path); + rcsbufp = &rcsbuf; + } + + RCS_deltas (rcs, fp, rcsbufp, rev, RCS_FETCH, &value, &len, + &log, &loglen); + free_value = 1; + } + + /* If OPTIONS is NULL or the empty string, then the old code would + invoke the RCS co program with no -k option, which means that + co would use the string we have stored in rcs->expand. */ + if ((options == NULL || options[0] == '\0') && rcs->expand == NULL) + expand = KFLAG_KV; + else + { + const char *ouroptions; + const char * const *cpp; + + if (options != NULL && options[0] != '\0') + { + assert (options[0] == '-' && options[1] == 'k'); + ouroptions = options + 2; + } + else + ouroptions = rcs->expand; + + for (cpp = kflags; *cpp != NULL; cpp++) + if (STREQ (*cpp, ouroptions)) + break; + + if (*cpp != NULL) + expand = (enum kflag) (cpp - kflags); + else + { + error (0, 0, + "internal error: unsupported substitution string -k%s", + ouroptions); + expand = KFLAG_KV; + } + } + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* Handle special files and permissions, if that is desired. */ + if (preserve_perms) + { + RCSVers *vers; + Node *info; + + vp = findnode (rcs->versions, rev == NULL ? rcs->head : rev); + if (vp == NULL) + error (1, 0, "internal error: no revision information for %s", + rev == NULL ? rcs->head : rev); + vers = (RCSVers *) vp->data; + + /* First we look for symlinks, which are simplest to handle. */ + info = findnode (vers->other_delta, "symlink"); + if (info != NULL) + { + char *dest; + + if (pfn != NULL || (workfile == NULL && sout == RUN_TTY)) + error (1, 0, "symbolic link %s:%s cannot be piped", + rcs->path, vers->version); + if (workfile == NULL) + dest = sout; + else + dest = workfile; + + /* Remove `dest', just in case. It's okay to get ENOENT here, + since we just want the file not to be there. (TODO: decide + whether it should be considered an error for `dest' to exist + at this point. If so, the unlink call should be removed and + `symlink' should signal the error. -twp) */ + if (unlink (dest) < 0 && !existence_error (errno)) + error (1, errno, "cannot remove %s", dest); + if (symlink (info->data, dest) < 0) + error (1, errno, "cannot create symbolic link from %s to %s", + dest, info->data); + if (free_value) + free (value); + if (free_rev) + free (rev); + return 0; + } + + /* Next, we look at this file's hardlinks field, and see whether + it is linked to any other file that has been checked out. + If so, we don't do anything else -- just link it to that file. + + If we are checking out a file to a pipe or temporary storage, + none of this should matter. Hence the `workfile != NULL' + wrapper around the whole thing. -twp */ + + if (workfile != NULL) + { + List *links = vers->hardlinks; + if (links != NULL) + { + Node *uptodate_link; + + /* For each file in the hardlinks field, check to see + if it exists, and if so, if it has been checked out + this iteration. When walklist returns, uptodate_link + should point to a hardlist node representing a file + in `links' which has recently been checked out, or + NULL if no file in `links' has yet been checked out. */ + + uptodate_link = NULL; + (void) walklist (links, find_checkedout_proc, &uptodate_link); + dellist (&links); + + /* If we've found a file that `workfile' is supposed to be + linked to, and it has been checked out since CVS was + invoked, then simply link workfile to that file and return. + + If one of these conditions is not met, then + workfile is the first one in its hardlink group to + be checked out, and we must continue with a full + checkout. */ + + if (uptodate_link != NULL) + { + struct hardlink_info *hlinfo = + (struct hardlink_info *) uptodate_link->data; + + if (link (uptodate_link->key, workfile) < 0) + error (1, errno, "cannot link %s to %s", + workfile, uptodate_link->key); + hlinfo->checked_out = 1; /* probably unnecessary */ + if (free_value) + free (value); + if (free_rev) + free (rev); + return 0; + } + } + } + + info = findnode (vers->other_delta, "owner"); + if (info != NULL) + { + change_rcs_owner_or_group = 1; + rcs_owner = (uid_t) strtoul (info->data, NULL, 10); + } + info = findnode (vers->other_delta, "group"); + if (info != NULL) + { + change_rcs_owner_or_group = 1; + rcs_group = (gid_t) strtoul (info->data, NULL, 10); + } + info = findnode (vers->other_delta, "permissions"); + if (info != NULL) + { + change_rcs_mode = 1; + rcs_mode = (mode_t) strtoul (info->data, NULL, 8); + } + info = findnode (vers->other_delta, "special"); + if (info != NULL) + { + /* If the size of `devtype' changes, fix the sscanf call also */ + char devtype[16]; + + if (sscanf (info->data, "%16s %lu", + devtype, &devnum_long) < 2) + error (1, 0, "%s:%s has bad `special' newphrase %s", + workfile, vers->version, info->data); + devnum = devnum_long; + if (STREQ (devtype, "character")) + special_file = S_IFCHR; + else if (STREQ (devtype, "block")) + special_file = S_IFBLK; + else + error (0, 0, "%s is a special file of unsupported type `%s'", + workfile, info->data); + } + } +#endif + + if (expand != KFLAG_O && expand != KFLAG_B) + { + char *newvalue; + + /* Don't fetch the delta node again if we already have it. */ + if (vp == NULL) + { + vp = findnode (rcs->versions, rev == NULL ? rcs->head : rev); + if (vp == NULL) + error (1, 0, "internal error: no revision information for %s", + rev == NULL ? rcs->head : rev); + } + + expand_keywords (rcs, (RCSVers *) vp->data, nametag, log, loglen, + expand, value, len, &newvalue, &len); + + if (newvalue != value) + { + if (free_value) + free (value); + value = newvalue; + free_value = 1; + } + } + + if (free_rev) + free (rev); + + if (log != NULL) + { + free (log); + log = NULL; + } + + if (pfn != NULL) + { +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (special_file) + error (1, 0, "special file %s cannot be piped to anything", + rcs->path); +#endif + /* The PFN interface is very simple to implement right now, as + we always have the entire file in memory. */ + if (len != 0) + pfn (callerdat, value, len); + } +#ifdef PRESERVE_PERMISSIONS_SUPPORT + else if (special_file) + { + char *dest; + + /* Can send either to WORKFILE or to SOUT, as long as SOUT is + not RUN_TTY. */ + dest = workfile; + if (dest == NULL) + { + if (sout == RUN_TTY) + error (1, 0, "special file %s cannot be written to stdout", + rcs->path); + dest = sout; + } + + /* Unlink `dest', just in case. It's okay if this provokes a + ENOENT error. */ + if (unlink (dest) < 0 && existence_error (errno)) + error (1, errno, "cannot remove %s", dest); + if (mknod (dest, special_file, devnum) < 0) + error (1, errno, "could not create special file %s", + dest); + } +#endif + else + { + /* Not a special file: write to WORKFILE or SOUT. */ + if (workfile == NULL) + { + if (sout == RUN_TTY) + ofp = stdout; + else + { + /* Symbolic links should be removed before replacement, so that + `fopen' doesn't follow the link and open the wrong file. */ + if (islink (sout)) + if (unlink_file (sout) < 0) + error (1, errno, "cannot remove %s", sout); + ofp = CVS_FOPEN (sout, expand == KFLAG_B ? "wb" : "w"); + if (ofp == NULL) + error (1, errno, "cannot open %s", sout); + } + } + else + { + /* Output is supposed to go to WORKFILE, so we should open that + file. Symbolic links should be removed first (see above). */ + if (islink (workfile)) + if (unlink_file (workfile) < 0) + error (1, errno, "cannot remove %s", workfile); + + ofp = CVS_FOPEN (workfile, expand == KFLAG_B ? "wb" : "w"); + + /* If the open failed because the existing workfile was not + writable, try to chmod the file and retry the open. */ + if (ofp == NULL && errno == EACCES + && isfile (workfile) && !iswritable (workfile)) + { + xchmod (workfile, 1); + ofp = CVS_FOPEN (workfile, expand == KFLAG_B ? "wb" : "w"); + } + + if (ofp == NULL) + { + error (0, errno, "cannot open %s", workfile); + if (free_value) + free (value); + return 1; + } + } + + if (workfile == NULL && sout == RUN_TTY) + { + if (expand == KFLAG_B) + cvs_output_binary (value, len); + else + { + /* cvs_output requires the caller to check for zero + length. */ + if (len > 0) + cvs_output (value, len); + } + } + else + { + /* NT 4.0 is said to have trouble writing 2099999 bytes + (for example) in a single fwrite. So break it down + (there is no need to be writing that much at once + anyway; it is possible that LARGEST_FWRITE should be + somewhat larger for good performance, but for testing I + want to start with a small value until/unless a bigger + one proves useful). */ +#define LARGEST_FWRITE 8192 + size_t nleft = len; + size_t nstep = (len < LARGEST_FWRITE ? len : LARGEST_FWRITE); + char *p = value; + + while (nleft > 0) + { + if (fwrite (p, 1, nstep, ofp) != nstep) + { + error (0, errno, "cannot write %s", + (workfile != NULL + ? workfile + : (sout != RUN_TTY ? sout : "stdout"))); + if (free_value) + free (value); + return 1; + } + p += nstep; + nleft -= nstep; + if (nleft < nstep) + nstep = nleft; + } + } + } + + if (free_value) + free (value); + + if (workfile != NULL) + { + int ret; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (!special_file && fclose (ofp) < 0) + { + error (0, errno, "cannot close %s", workfile); + return 1; + } + + if (change_rcs_owner_or_group) + { + if (chown (workfile, rcs_owner, rcs_group) < 0) + error (0, errno, "could not change owner or group of %s", + workfile); + } + + ret = chmod (workfile, + change_rcs_mode + ? rcs_mode + : sb.st_mode & ~(S_IWRITE | S_IWGRP | S_IWOTH)); +#else + if (fclose (ofp) < 0) + { + error (0, errno, "cannot close %s", workfile); + return 1; + } + + ret = chmod (workfile, + sb.st_mode & ~(S_IWRITE | S_IWGRP | S_IWOTH)); +#endif + if (ret < 0) + { + error (0, errno, "cannot change mode of file %s", + workfile); + } + } + else if (sout != RUN_TTY) + { + if ( +#ifdef PRESERVE_PERMISSIONS_SUPPORT + !special_file && +#endif + fclose (ofp) < 0) + { + error (0, errno, "cannot close %s", sout); + return 1; + } + } + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* If we are in the business of preserving hardlinks, then + mark this file as having been checked out. */ + if (preserve_perms && workfile != NULL) + update_hardlink_info (workfile); +#endif + + return 0; +} + +static RCSVers *RCS_findlock_or_tip PROTO ((RCSNode *rcs)); + +/* Find the delta currently locked by the user. From the `ci' man page: + + "If rev is omitted, ci tries to derive the new revision + number from the caller's last lock. If the caller has + locked the tip revision of a branch, the new revision is + appended to that branch. The new revision number is + obtained by incrementing the tip revision number. If the + caller locked a non-tip revision, a new branch is started + at that revision by incrementing the highest branch number + at that revision. The default initial branch and level + numbers are 1. + + If rev is omitted and the caller has no lock, but owns the + file and locking is not set to strict, then the revision + is appended to the default branch (normally the trunk; see + the -b option of rcs(1))." + + RCS_findlock_or_tip finds the unique revision locked by the caller + and returns its delta node. If the caller has not locked any + revisions (and is permitted to commit to an unlocked delta, as + described above), return the tip of the default branch. */ + +static RCSVers * +RCS_findlock_or_tip (rcs) + RCSNode *rcs; +{ + char *user = getcaller(); + Node *lock, *p; + List *locklist; + + /* Find unique delta locked by caller. This code is very similar + to the code in RCS_unlock -- perhaps it could be abstracted + into a RCS_findlock function. */ + locklist = RCS_getlocks (rcs); + lock = NULL; + for (p = locklist->list->next; p != locklist->list; p = p->next) + { + if (STREQ (p->data, user)) + { + if (lock != NULL) + { + error (0, 0, "\ +%s: multiple revisions locked by %s; please specify one", rcs->path, user); + return NULL; + } + lock = p; + } + } + + if (lock != NULL) + { + /* Found an old lock, but check that the revision still exists. */ + p = findnode (rcs->versions, lock->key); + if (p == NULL) + { + error (0, 0, "%s: can't unlock nonexistent revision %s", + rcs->path, + lock->key); + return NULL; + } + return (RCSVers *) p->data; + } + + /* No existing lock. The RCS rule is that this is an error unless + locking is nonstrict AND the file is owned by the current + user. Trying to determine the latter is a portability nightmare + in the face of NT, VMS, AFS, and other systems with non-unix-like + ideas of users and owners. In the case of CVS, we should never get + here (as long as the traditional behavior of making sure to call + RCS_lock persists). Anyway, we skip the RCS error checks + and just return the default branch or head. The reasoning is that + those error checks are to make users lock before a checkin, and we do + that in other ways if at all anyway (e.g. rcslock.pl). */ + + p = findnode (rcs->versions, RCS_getbranch (rcs, rcs->branch, 0)); + return (RCSVers *) p->data; +} + +/* Revision number string, R, must contain a `.'. + Return a newly-malloc'd copy of the prefix of R up + to but not including the final `.'. */ + +static char * +truncate_revnum (r) + const char *r; +{ + size_t len; + char *new_r; + char *dot = strrchr (r, '.'); + + assert (dot); + len = dot - r; + new_r = xmalloc (len + 1); + memcpy (new_r, r, len); + *(new_r + len) = '\0'; + return new_r; +} + +/* Revision number string, R, must contain a `.'. + R must be writable. Replace the rightmost `.' in R with + the NUL byte and return a pointer to that NUL byte. */ + +static char * +truncate_revnum_in_place (r) + char *r; +{ + char *dot = strrchr (r, '.'); + assert (dot); + *dot = '\0'; + return dot; +} + +/* Revision number strings, R and S, must each contain a `.'. + R and S must be writable and must have the same number of dots. + Truncate R and S for the comparison, then restored them to their + original state. + Return the result (see compare_revnums) of comparing R and S + ignoring differences in any component after the rightmost `.'. */ + +static int +compare_truncated_revnums (r, s) + char *r; + char *s; +{ + char *r_dot = truncate_revnum_in_place (r); + char *s_dot = truncate_revnum_in_place (s); + int cmp; + + assert (numdots (r) == numdots (s)); + + cmp = compare_revnums (r, s); + + *r_dot = '.'; + *s_dot = '.'; + + return cmp; +} + +/* Return a malloc'd copy of the string representing the highest branch + number on BRANCHNODE. If there are no branches on BRANCHNODE, return NULL. + FIXME: isn't the max rev always the last one? + If so, we don't even need a loop. */ + +static char *max_rev PROTO ((const RCSVers *)); + +static char * +max_rev (branchnode) + const RCSVers *branchnode; +{ + Node *head; + Node *bp; + char *max; + + if (branchnode->branches == NULL) + { + return NULL; + } + + max = NULL; + head = branchnode->branches->list; + for (bp = head->next; bp != head; bp = bp->next) + { + if (max == NULL || compare_truncated_revnums (max, bp->key) < 0) + { + max = bp->key; + } + } + assert (max); + + return truncate_revnum (max); +} + +/* Create BRANCH in RCS's delta tree. BRANCH may be either a branch + number or a revision number. In the former case, create the branch + with the specified number; in the latter case, create a new branch + rooted at node BRANCH with a higher branch number than any others. + Return the number of the tip node on the new branch. */ + +static char * +RCS_addbranch (rcs, branch) + RCSNode *rcs; + const char *branch; +{ + char *branchpoint, *newrevnum; + Node *nodep, *bp; + Node *marker; + RCSVers *branchnode; + + /* Append to end by default. */ + marker = NULL; + + branchpoint = xstrdup (branch); + if ((numdots (branchpoint) & 1) == 0) + { + truncate_revnum_in_place (branchpoint); + } + + /* Find the branch rooted at BRANCHPOINT. */ + nodep = findnode (rcs->versions, branchpoint); + if (nodep == NULL) + { + error (0, 0, "%s: can't find branch point %s", rcs->path, branchpoint); + return NULL; + } + branchnode = (RCSVers *) nodep->data; + + /* If BRANCH was a full branch number, make sure it is higher than MAX. */ + if ((numdots (branch) & 1) == 1) + { + if (branchnode->branches == NULL) + { + /* We have to create the first branch on this node, which means + appending ".2" to the revision number. */ + newrevnum = (char *) xmalloc (strlen (branch) + 3); + strcpy (newrevnum, branch); + strcat (newrevnum, ".2"); + } + else + { + char *max = max_rev (branchnode); + assert (max); + newrevnum = increment_revnum (max); + free (max); + } + } + else + { + newrevnum = xstrdup (branch); + + if (branchnode->branches != NULL) + { + Node *head; + Node *bp; + + /* Find the position of this new branch in the sorted list + of branches. */ + head = branchnode->branches->list; + for (bp = head->next; bp != head; bp = bp->next) + { + char *dot; + int found_pos; + + /* The existing list must be sorted on increasing revnum. */ + assert (bp->next == head + || compare_truncated_revnums (bp->key, + bp->next->key) < 0); + dot = truncate_revnum_in_place (bp->key); + found_pos = (compare_revnums (branch, bp->key) < 0); + *dot = '.'; + + if (found_pos) + { + break; + } + } + marker = bp; + } + } + + newrevnum = (char *) xrealloc (newrevnum, strlen (newrevnum) + 3); + strcat (newrevnum, ".1"); + + /* Add this new revision number to BRANCHPOINT's branches list. */ + if (branchnode->branches == NULL) + branchnode->branches = getlist(); + bp = getnode(); + bp->key = xstrdup (newrevnum); + + /* Append to the end of the list by default, that is, just before + the header node, `list'. */ + if (marker == NULL) + marker = branchnode->branches->list; + + { + int fail; + fail = insert_before (branchnode->branches, marker, bp); + assert (!fail); + } + + return newrevnum; +} + +/* Check in to RCSFILE with revision REV (which must be greater than + the largest revision) and message MESSAGE (which is checked for + legality). If FLAGS & RCS_FLAGS_DEAD, check in a dead revision. + If FLAGS & RCS_FLAGS_QUIET, tell ci to be quiet. If FLAGS & + RCS_FLAGS_MODTIME, use the working file's modification time for the + checkin time. WORKFILE is the working file to check in from, or + NULL to use the usual RCS rules for deriving it from the RCSFILE. + If FLAGS & RCS_FLAGS_KEEPFILE, don't unlink the working file; + unlinking the working file is standard RCS behavior, but is rarely + appropriate for CVS. + + This function should almost exactly mimic the behavior of `rcs ci'. The + principal point of difference is the support here for preserving file + ownership and permissions in the delta nodes. This is not a clean + solution -- precisely because it diverges from RCS's behavior -- but + it doesn't seem feasible to do this anywhere else in the code. [-twp] + + Return value is -1 for error (and errno is set to indicate the + error), positive for error (and an error message has been printed), + or zero for success. */ + +int +RCS_checkin (rcs, workfile, message, rev, flags) + RCSNode *rcs; + char *workfile; + char *message; + char *rev; + int flags; +{ + RCSVers *delta, *commitpt; + Deltatext *dtext; + Node *nodep; + char *tmpfile, *changefile, *chtext; + char *diffopts; + size_t bufsize; + int buflen, chtextlen; + int status, checkin_quiet, allocated_workfile; + struct tm *ftm; + time_t modtime; + int adding_branch = 0; +#ifdef PRESERVE_PERMISSIONS_SUPPORT + struct stat sb; +#endif + + commitpt = NULL; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* Get basename of working file. Is there a library function to + do this? I couldn't find one. -twp */ + allocated_workfile = 0; + if (workfile == NULL) + { + char *p; + int extlen = strlen (RCSEXT); + workfile = xstrdup (last_component (rcs->path)); + p = workfile + (strlen (workfile) - extlen); + assert (strncmp (p, RCSEXT, extlen) == 0); + *p = '\0'; + allocated_workfile = 1; + } + + /* If the filename is a symbolic link, follow it and replace it + with the destination of the link. We need to do this before + calling rcs_internal_lockfile, or else we won't put the lock in + the right place. */ + resolve_symlink (&(rcs->path)); + + checkin_quiet = flags & RCS_FLAGS_QUIET; + if (!checkin_quiet) + { + cvs_output (rcs->path, 0); + cvs_output (" <-- ", 7); + cvs_output (workfile, 0); + cvs_output ("\n", 1); + } + + /* Create new delta node. */ + delta = (RCSVers *) xmalloc (sizeof (RCSVers)); + memset (delta, 0, sizeof (RCSVers)); + delta->author = xstrdup (getcaller ()); + if (flags & RCS_FLAGS_MODTIME) + { + struct stat ws; + if (stat (workfile, &ws) < 0) + { + error (1, errno, "cannot stat %s", workfile); + } + modtime = ws.st_mtime; + } + else + (void) time (&modtime); + ftm = gmtime (&modtime); + delta->date = (char *) xmalloc (MAXDATELEN); + (void) sprintf (delta->date, DATEFORM, + ftm->tm_year + (ftm->tm_year < 100 ? 0 : 1900), + ftm->tm_mon + 1, ftm->tm_mday, ftm->tm_hour, + ftm->tm_min, ftm->tm_sec); + if (flags & RCS_FLAGS_DEAD) + { + delta->state = xstrdup (RCSDEAD); + delta->dead = 1; + } + else + delta->state = xstrdup ("Exp"); + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* If permissions should be preserved on this project, then + save the permission info. */ + if (preserve_perms) + { + Node *np; + char buf[64]; /* static buffer should be safe: see usage. -twp */ + + delta->other_delta = getlist(); + + if (CVS_LSTAT (workfile, &sb) < 0) + error (1, 1, "cannot lstat %s", workfile); + + if (S_ISLNK (sb.st_mode)) + { + np = getnode(); + np->key = xstrdup ("symlink"); + np->data = xreadlink (workfile); + addnode (delta->other_delta, np); + } + else + { + (void) sprintf (buf, "%u", sb.st_uid); + np = getnode(); + np->key = xstrdup ("owner"); + np->data = xstrdup (buf); + addnode (delta->other_delta, np); + + (void) sprintf (buf, "%u", sb.st_gid); + np = getnode(); + np->key = xstrdup ("group"); + np->data = xstrdup (buf); + addnode (delta->other_delta, np); + + (void) sprintf (buf, "%o", sb.st_mode & 07777); + np = getnode(); + np->key = xstrdup ("permissions"); + np->data = xstrdup (buf); + addnode (delta->other_delta, np); + + /* Save device number. */ + switch (sb.st_mode & S_IFMT) + { + case S_IFREG: break; + case S_IFCHR: + case S_IFBLK: + np = getnode(); + np->key = xstrdup ("special"); + sprintf (buf, "%s %lu", + ((sb.st_mode & S_IFMT) == S_IFCHR + ? "character" : "block"), + (unsigned long) sb.st_rdev); + np->data = xstrdup (buf); + addnode (delta->other_delta, np); + break; + + default: + error (0, 0, "special file %s has unknown type", workfile); + } + + /* Save hardlinks. */ + delta->hardlinks = list_linked_files_on_disk (workfile); + } + } +#endif + + /* Create a new deltatext node. */ + dtext = (Deltatext *) xmalloc (sizeof (Deltatext)); + memset (dtext, 0, sizeof (Deltatext)); + + dtext->log = make_message_rcslegal (message); + + /* If the delta tree is empty, then there's nothing to link the + new delta into. So make a new delta tree, snarf the working + file contents, and just write the new RCS file. */ + if (rcs->head == NULL) + { + char *newrev; + FILE *fout; + + /* Figure out what the first revision number should be. */ + if (rev == NULL || *rev == '\0') + newrev = xstrdup ("1.1"); + else if (numdots (rev) == 0) + { + newrev = (char *) xmalloc (strlen (rev) + 3); + strcpy (newrev, rev); + strcat (newrev, ".1"); + } + else + newrev = xstrdup (rev); + + /* Don't need to xstrdup NEWREV because it's already dynamic, and + not used for anything else. (Don't need to free it, either.) */ + rcs->head = newrev; + delta->version = xstrdup (newrev); + nodep = getnode(); + nodep->type = RCSVERS; + nodep->key = xstrdup (newrev); + nodep->data = (char *) delta; + (void) addnode (rcs->versions, nodep); + + dtext->version = xstrdup (newrev); + bufsize = 0; +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (preserve_perms && !S_ISREG (sb.st_mode)) + /* Pretend file is empty. */ + bufsize = 0; + else +#endif + get_file (workfile, workfile, + rcs->expand != NULL && STREQ (rcs->expand, "b") ? "rb" : "r", + &dtext->text, &bufsize, &dtext->len); + + if (!checkin_quiet) + { + cvs_output ("initial revision: ", 0); + cvs_output (rcs->head, 0); + cvs_output ("\n", 1); + } + + /* We are probably about to invalidate any cached file. */ + rcsbuf_cache_close (); + + fout = rcs_internal_lockfile (rcs->path); + RCS_putadmin (rcs, fout); + RCS_putdtree (rcs, rcs->head, fout); + RCS_putdesc (rcs, fout); + rcs->delta_pos = ftell (fout); + if (rcs->delta_pos == -1) + error (1, errno, "cannot ftell for %s", rcs->path); + putdeltatext (fout, dtext); + rcs_internal_unlockfile (fout, rcs->path); + freedeltatext (dtext); + + if ((flags & RCS_FLAGS_KEEPFILE) == 0) + { + if (unlink_file (workfile) < 0) + /* FIXME-update-dir: message does not include update_dir. */ + error (0, errno, "cannot remove %s", workfile); + } + + if (!checkin_quiet) + cvs_output ("done\n", 5); + + return 0; + } + + /* Derive a new revision number. From the `ci' man page: + + "If rev is a revision number, it must be higher than the + latest one on the branch to which rev belongs, or must + start a new branch. + + If rev is a branch rather than a revision number, the new + revision is appended to that branch. The level number is + obtained by incrementing the tip revision number of that + branch. If rev indicates a non-existing branch, that + branch is created with the initial revision numbered + rev.1." + + RCS_findlock_or_tip handles the case where REV is omitted. + RCS 5.7 also permits REV to be "$" or to begin with a dot, but + we do not address those cases -- every routine that calls + RCS_checkin passes it a numeric revision. */ + + if (rev == NULL || *rev == '\0') + { + /* Figure out where the commit point is by looking for locks. + If the commit point is at the tip of a branch (or is the + head of the delta tree), then increment its revision number + to obtain the new revnum. Otherwise, start a new + branch. */ + commitpt = RCS_findlock_or_tip (rcs); + if (commitpt == NULL) + { + status = 1; + goto checkin_done; + } + else if (commitpt->next == NULL + || STREQ (commitpt->version, rcs->head)) + delta->version = increment_revnum (commitpt->version); + else + delta->version = RCS_addbranch (rcs, commitpt->version); + } + else + { + /* REV is either a revision number or a branch number. Find the + tip of the target branch. */ + char *branch, *tip, *newrev, *p; + int dots, isrevnum; + + assert (isdigit ((unsigned char) *rev)); + + newrev = xstrdup (rev); + dots = numdots (newrev); + isrevnum = dots & 1; + + branch = xstrdup (rev); + if (isrevnum) + { + p = strrchr (branch, '.'); + *p = '\0'; + } + + /* Find the tip of the target branch. If we got a one- or two-digit + revision number, this will be the head of the tree. Exception: + if rev is a single-field revision equal to the branch number of + the trunk (usually "1") then we want to treat it like an ordinary + branch revision. */ + if (dots == 0) + { + tip = xstrdup (rcs->head); + if (atoi (tip) != atoi (branch)) + { + newrev = (char *) xrealloc (newrev, strlen (newrev) + 3); + strcat (newrev, ".1"); + dots = isrevnum = 1; + } + } + else if (dots == 1) + tip = xstrdup (rcs->head); + else + tip = RCS_getbranch (rcs, branch, 1); + + /* If the branch does not exist, and we were supplied an exact + revision number, signal an error. Otherwise, if we were + given only a branch number, create it and set COMMITPT to + the branch point. */ + if (tip == NULL) + { + if (isrevnum) + { + error (0, 0, "%s: can't find branch point %s", + rcs->path, branch); + free (branch); + free (newrev); + status = 1; + goto checkin_done; + } + delta->version = RCS_addbranch (rcs, branch); + if (!delta->version) + { + free (branch); + free (newrev); + status = 1; + goto checkin_done; + } + adding_branch = 1; + p = strrchr (branch, '.'); + *p = '\0'; + tip = xstrdup (branch); + } + else + { + if (isrevnum) + { + /* NEWREV must be higher than TIP. */ + if (compare_revnums (tip, newrev) >= 0) + { + error (0, 0, + "%s: revision %s too low; must be higher than %s", + rcs->path, + newrev, tip); + free (branch); + free (newrev); + free (tip); + status = 1; + goto checkin_done; + } + delta->version = xstrdup (newrev); + } + else + /* Just increment the tip number to get the new revision. */ + delta->version = increment_revnum (tip); + } + + nodep = findnode (rcs->versions, tip); + commitpt = (RCSVers *) nodep->data; + + free (branch); + free (newrev); + free (tip); + } + + assert (delta->version != NULL); + + /* If COMMITPT is locked by us, break the lock. If it's locked + by someone else, signal an error. */ + nodep = findnode (RCS_getlocks (rcs), commitpt->version); + if (nodep != NULL) + { + if (! STREQ (nodep->data, delta->author)) + { + /* If we are adding a branch, then leave the old lock around. + That is sensible in the sense that when adding a branch, + we don't need to use the lock to tell us where to check + in. It is fishy in the sense that if it is our own lock, + we break it. However, this is the RCS 5.7 behavior (at + the end of addbranch in ci.c in RCS 5.7, it calls + removelock only if it is our own lock, not someone + else's). */ + + if (!adding_branch) + { + error (0, 0, "%s: revision %s locked by %s", + rcs->path, + nodep->key, nodep->data); + status = 1; + goto checkin_done; + } + } + else + delnode (nodep); + } + + dtext->version = xstrdup (delta->version); + + /* Obtain the change text for the new delta. If DELTA is to be the + new head of the tree, then its change text should be the contents + of the working file, and LEAFNODE's change text should be a diff. + Else, DELTA's change text should be a diff between LEAFNODE and + the working file. */ + + tmpfile = cvs_temp_name(); + status = RCS_checkout (rcs, NULL, commitpt->version, NULL, + ((rcs->expand != NULL + && STREQ (rcs->expand, "b")) + ? "-kb" + : "-ko"), + tmpfile, + (RCSCHECKOUTPROC)0, NULL); + if (status != 0) + error (1, 0, + "could not check out revision %s of `%s'", + commitpt->version, rcs->path); + + bufsize = buflen = 0; + chtext = NULL; + chtextlen = 0; + changefile = cvs_temp_name(); + + /* Diff options should include --binary if the RCS file has -kb set + in its `expand' field. */ + diffopts = (rcs->expand != NULL && STREQ (rcs->expand, "b") + ? "-a -n --binary" + : "-a -n"); + + if (STREQ (commitpt->version, rcs->head) && + numdots (delta->version) == 1) + { + /* If this revision is being inserted on the trunk, the change text + for the new delta should be the contents of the working file ... */ + bufsize = 0; +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (preserve_perms && !S_ISREG (sb.st_mode)) + /* Pretend file is empty. */ + ; + else +#endif + get_file (workfile, workfile, + rcs->expand != NULL && STREQ (rcs->expand, "b") ? "rb" : "r", + &dtext->text, &bufsize, &dtext->len); + + /* ... and the change text for the old delta should be a diff. */ + commitpt->text = (Deltatext *) xmalloc (sizeof (Deltatext)); + memset (commitpt->text, 0, sizeof (Deltatext)); + + bufsize = 0; + switch (diff_exec (workfile, tmpfile, diffopts, changefile)) + { + case 0: + case 1: + break; + case -1: + /* FIXME-update-dir: message does not include update_dir. */ + error (1, errno, "error diffing %s", workfile); + break; + default: + /* FIXME-update-dir: message does not include update_dir. */ + error (1, 0, "error diffing %s", workfile); + break; + } + + /* OK, the text file case here is really dumb. Logically + speaking we want diff to read the files in text mode, + convert them to the canonical form found in RCS files + (which, we hope at least, is independent of OS--always + bare linefeeds), and then work with change texts in that + format. However, diff_exec both generates change + texts and produces output for user purposes (e.g. patch.c), + and there is no way to distinguish between the two cases. + So we actually implement the text file case by writing the + change text as a text file, then reading it as a text file. + This should cause no harm, but doesn't strike me as + immensely clean. */ + get_file (changefile, changefile, + rcs->expand != NULL && STREQ (rcs->expand, "b") ? "rb" : "r", + &commitpt->text->text, &bufsize, &commitpt->text->len); + + /* If COMMITPT->TEXT->TEXT is NULL, it means that CHANGEFILE + was empty and that there are no differences between revisions. + In that event, we want to force RCS_rewrite to write an empty + string for COMMITPT's change text. Leaving the change text + field set NULL won't work, since that means "preserve the original + change text for this delta." */ + if (commitpt->text->text == NULL) + { + commitpt->text->text = xstrdup (""); + commitpt->text->len = 0; + } + } + else + { + /* This file is not being inserted at the head, but on a side + branch somewhere. Make a diff from the previous revision + to the working file. */ + switch (diff_exec (tmpfile, workfile, diffopts, changefile)) + { + case 0: + case 1: + break; + case -1: + /* FIXME-update-dir: message does not include update_dir. */ + error (1, errno, "error diffing %s", workfile); + break; + default: + /* FIXME-update-dir: message does not include update_dir. */ + error (1, 0, "error diffing %s", workfile); + break; + } + /* See the comment above, at the other get_file invocation, + regarding binary vs. text. */ + get_file (changefile, changefile, + rcs->expand != NULL && STREQ (rcs->expand, "b") ? "rb" : "r", + &dtext->text, &bufsize, + &dtext->len); + if (dtext->text == NULL) + { + dtext->text = xstrdup (""); + dtext->len = 0; + } + } + + /* Update DELTA linkage. It is important not to do this before + the very end of RCS_checkin; if an error arises that forces + us to abort checking in, we must not have malformed deltas + partially linked into the tree. + + If DELTA and COMMITPT are on different branches, do nothing -- + DELTA is linked to the tree through COMMITPT->BRANCHES, and we + don't want to change `next' pointers. + + Otherwise, if the nodes are both on the trunk, link DELTA to + COMMITPT; otherwise, link COMMITPT to DELTA. */ + + if (numdots (commitpt->version) == numdots (delta->version)) + { + if (STREQ (commitpt->version, rcs->head)) + { + delta->next = rcs->head; + rcs->head = xstrdup (delta->version); + } + else + commitpt->next = xstrdup (delta->version); + } + + /* Add DELTA to RCS->VERSIONS. */ + if (rcs->versions == NULL) + rcs->versions = getlist(); + nodep = getnode(); + nodep->type = RCSVERS; + nodep->key = xstrdup (delta->version); + nodep->data = (char *) delta; + (void) addnode (rcs->versions, nodep); + + /* Write the new RCS file, inserting the new delta at COMMITPT. */ + if (!checkin_quiet) + { + cvs_output ("new revision: ", 14); + cvs_output (delta->version, 0); + cvs_output ("; previous revision: ", 21); + cvs_output (commitpt->version, 0); + cvs_output ("\n", 1); + } + + RCS_rewrite (rcs, dtext, commitpt->version); + + if ((flags & RCS_FLAGS_KEEPFILE) == 0) + { + if (unlink_file (workfile) < 0) + /* FIXME-update-dir: message does not include update_dir. */ + error (1, errno, "cannot remove %s", workfile); + } + if (unlink_file (tmpfile) < 0) + error (0, errno, "cannot remove %s", tmpfile); + if (unlink_file (changefile) < 0) + error (0, errno, "cannot remove %s", changefile); + + if (!checkin_quiet) + cvs_output ("done\n", 5); + + checkin_done: + if (allocated_workfile) + free (workfile); + + if (commitpt != NULL && commitpt->text != NULL) + { + freedeltatext (commitpt->text); + commitpt->text = NULL; + } + + freedeltatext (dtext); + if (status != 0) + free_rcsvers_contents (delta); + + return status; +} + +/* This structure is passed between RCS_cmp_file and cmp_file_buffer. */ + +struct cmp_file_data +{ + const char *filename; + FILE *fp; + int different; +}; + +/* Compare the contents of revision REV of RCS file RCS with the + contents of the file FILENAME. OPTIONS is a string for the keyword + expansion options. Return 0 if the contents of the revision are + the same as the contents of the file, 1 if they are different. */ + +int +RCS_cmp_file (rcs, rev, options, filename) + RCSNode *rcs; + char *rev; + char *options; + const char *filename; +{ + int binary; + FILE *fp; + struct cmp_file_data data; + int retcode; + + if (options != NULL && options[0] != '\0') + binary = STREQ (options, "-kb"); + else + { + char *expand; + + expand = RCS_getexpand (rcs); + if (expand != NULL && STREQ (expand, "b")) + binary = 1; + else + binary = 0; + } + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* If CVS is to deal properly with special files (when + PreservePermissions is on), the best way is to check out the + revision to a temporary file and call `xcmp' on the two disk + files. xcmp needs to handle non-regular files properly anyway, + so calling it simplifies RCS_cmp_file. We *could* just yank + the delta node out of the version tree and look for device + numbers, but writing to disk and calling xcmp is a better + abstraction (therefore probably more robust). -twp */ + + if (preserve_perms) + { + char *tmp; + + tmp = cvs_temp_name(); + retcode = RCS_checkout(rcs, NULL, rev, NULL, options, tmp, NULL, NULL); + if (retcode != 0) + return 1; + + retcode = xcmp (tmp, filename); + if (CVS_UNLINK (tmp) < 0) + error (0, errno, "cannot remove %s", tmp); + return retcode; + } + else +#endif + { + fp = CVS_FOPEN (filename, binary ? FOPEN_BINARY_READ : "r"); + if (fp == NULL) + /* FIXME-update-dir: should include update_dir in message. */ + error (1, errno, "cannot open file %s for comparing", filename); + + data.filename = filename; + data.fp = fp; + data.different = 0; + + retcode = RCS_checkout (rcs, (char *) NULL, rev, (char *) NULL, + options, RUN_TTY, cmp_file_buffer, + (void *) &data); + + /* If we have not yet found a difference, make sure that we are at + the end of the file. */ + if (! data.different) + { + if (getc (fp) != EOF) + data.different = 1; + } + + fclose (fp); + + if (retcode != 0) + return 1; + + return data.different; + } +} + +/* This is a subroutine of RCS_cmp_file. It is passed to + RCS_checkout. */ + +#define CMP_BUF_SIZE (8 * 1024) + +static void +cmp_file_buffer (callerdat, buffer, len) + void *callerdat; + const char *buffer; + size_t len; +{ + struct cmp_file_data *data = (struct cmp_file_data *) callerdat; + char *filebuf; + + /* If we've already found a difference, we don't need to check + further. */ + if (data->different) + return; + + filebuf = xmalloc (len > CMP_BUF_SIZE ? CMP_BUF_SIZE : len); + + while (len > 0) + { + size_t checklen; + + checklen = len > CMP_BUF_SIZE ? CMP_BUF_SIZE : len; + if (fread (filebuf, 1, checklen, data->fp) != checklen) + { + if (ferror (data->fp)) + error (1, errno, "cannot read file %s for comparing", + data->filename); + data->different = 1; + free (filebuf); + return; + } + + if (memcmp (filebuf, buffer, checklen) != 0) + { + data->different = 1; + free (filebuf); + return; + } + + buffer += checklen; + len -= checklen; + } + + free (filebuf); +} + +/* For RCS file RCS, make symbolic tag TAG point to revision REV. + This validates that TAG is OK for a user to use. Return value is + -1 for error (and errno is set to indicate the error), positive for + error (and an error message has been printed), or zero for success. */ + +int +RCS_settag (rcs, tag, rev) + RCSNode *rcs; + const char *tag; + const char *rev; +{ + List *symbols; + Node *node; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* FIXME: This check should be moved to RCS_check_tag. There is no + reason for it to be here. */ + if (STREQ (tag, TAG_BASE) + || STREQ (tag, TAG_HEAD)) + { + /* Print the name of the tag might be considered redundant + with the caller, which also prints it. Perhaps this helps + clarify why the tag name is considered reserved, I don't + know. */ + error (0, 0, "Attempt to add reserved tag name %s", tag); + return 1; + } + + /* A revision number of NULL means use the head or default branch. + If rev is not NULL, it may be a symbolic tag or branch number; + expand it to the correct numeric revision or branch head. */ + if (rev == NULL) + rev = rcs->branch ? rcs->branch : rcs->head; + + /* At this point rcs->symbol_data may not have been parsed. + Calling RCS_symbols will force it to be parsed into a list + which we can easily manipulate. */ + symbols = RCS_symbols (rcs); + if (symbols == NULL) + { + symbols = getlist (); + rcs->symbols = symbols; + } + node = findnode (symbols, tag); + if (node != NULL) + { + free (node->data); + node->data = xstrdup (rev); + } + else + { + node = getnode (); + node->key = xstrdup (tag); + node->data = xstrdup (rev); + (void) addnode_at_front (symbols, node); + } + + return 0; +} + +/* Delete the symbolic tag TAG from the RCS file RCS. Return 0 if + the tag was found (and removed), or 1 if it was not present. (In + either case, the tag will no longer be in RCS->SYMBOLS.) */ + +int +RCS_deltag (rcs, tag) + RCSNode *rcs; + const char *tag; +{ + List *symbols; + Node *node; + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + symbols = RCS_symbols (rcs); + if (symbols == NULL) + return 1; + + node = findnode (symbols, tag); + if (node == NULL) + return 1; + + delnode (node); + + return 0; +} + +/* Set the default branch of RCS to REV. */ + +int +RCS_setbranch (rcs, rev) + RCSNode *rcs; + const char *rev; +{ + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rev && ! *rev) + rev = NULL; + + if (rev == NULL && rcs->branch == NULL) + return 0; + if (rev != NULL && rcs->branch != NULL && STREQ (rev, rcs->branch)) + return 0; + + if (rcs->branch != NULL) + free (rcs->branch); + rcs->branch = xstrdup (rev); + + return 0; +} + +/* Lock revision REV. LOCK_QUIET is 1 to suppress output. FIXME: + Most of the callers only call us because RCS_checkin still tends to + like a lock (a relic of old behavior inherited from the RCS ci + program). If we clean this up, only "cvs admin -l" will still need + to call RCS_lock. */ + +/* FIXME-twp: if a lock owned by someone else is broken, should this + send mail to the lock owner? Prompt user? It seems like such an + obscure situation for CVS as almost not worth worrying much + about. */ + +int +RCS_lock (rcs, rev, lock_quiet) + RCSNode *rcs; + const char *rev; + int lock_quiet; +{ + List *locks; + Node *p; + char *user; + char *xrev = NULL; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + locks = RCS_getlocks (rcs); + if (locks == NULL) + locks = rcs->locks = getlist(); + user = getcaller(); + + /* A revision number of NULL means lock the head or default branch. */ + if (rev == NULL) + xrev = RCS_head (rcs); + + /* If rev is a branch number, lock the latest revision on that + branch. I think that if the branch doesn't exist, it's + okay to return 0 -- that just means that the branch is new, + so we don't need to lock it anyway. -twp */ + else if (RCS_nodeisbranch (rcs, rev)) + { + xrev = RCS_getbranch (rcs, (char *) rev, 1); + if (xrev == NULL) + { + if (!lock_quiet) + error (0, 0, "%s: branch %s absent", rcs->path, rev); + return 1; + } + } + + if (xrev == NULL) + xrev = xstrdup (rev); + + /* Make sure that the desired revision exists. Technically, + we can update the locks list without even checking this, + but RCS 5.7 did this. And it can't hurt. */ + if (findnode (rcs->versions, xrev) == NULL) + { + if (!lock_quiet) + error (0, 0, "%s: revision %s absent", rcs->path, xrev); + free (xrev); + return 1; + } + + /* Is this rev already locked? */ + p = findnode (locks, xrev); + if (p != NULL) + { + if (STREQ (p->data, user)) + { + /* We already own the lock on this revision, so do nothing. */ + free (xrev); + return 0; + } + +#if 0 + /* Well, first of all, "rev" below should be "xrev" to avoid + core dumps. But more importantly, should we really be + breaking the lock unconditionally? What CVS 1.9 does (via + RCS) is to prompt "Revision 1.1 is already locked by fred. + Do you want to break the lock? [ny](n): ". Well, we don't + want to interact with the user (certainly not at the + server/protocol level, and probably not in the command-line + client), but isn't it more sensible to give an error and + let the user run "cvs admin -u" if they want to break the + lock? */ + + /* Break the lock. */ + if (!lock_quiet) + { + cvs_output (rev, 0); + cvs_output (" unlocked\n", 0); + } + delnode (p); +#else + error (1, 0, "Revision %s is already locked by %s", xrev, p->data); +#endif + } + + /* Create a new lock. */ + p = getnode(); + p->key = xrev; /* already xstrdupped */ + p->data = xstrdup (getcaller()); + (void) addnode_at_front (locks, p); + + if (!lock_quiet) + { + cvs_output (xrev, 0); + cvs_output (" locked\n", 0); + } + + return 0; +} + +/* Unlock revision REV. UNLOCK_QUIET is 1 to suppress output. FIXME: + Like RCS_lock, this can become a no-op if we do the checkin + ourselves. + + If REV is not null and is locked by someone else, break their + lock and notify them. It is an open issue whether RCS_unlock + queries the user about whether or not to break the lock. */ + +int +RCS_unlock (rcs, rev, unlock_quiet) + RCSNode *rcs; + const char *rev; + int unlock_quiet; +{ + Node *lock; + List *locks; + char *user; + char *xrev = NULL; + + user = getcaller(); + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + /* If rev is NULL, unlock the latest revision (first in + rcs->locks) held by the caller. */ + if (rev == NULL) + { + Node *p; + + /* No-ops: attempts to unlock an empty tree or an unlocked file. */ + if (rcs->head == NULL) + { + if (!unlock_quiet) + cvs_outerr ("can't unlock an empty tree\n", 0); + return 0; + } + + locks = RCS_getlocks (rcs); + if (locks == NULL) + { + if (!unlock_quiet) + cvs_outerr ("No locks are set.\n", 0); + return 0; + } + + lock = NULL; + for (p = locks->list->next; p != locks->list; p = p->next) + { + if (lock != NULL) + { + if (!unlock_quiet) + error (0, 0, "\ +%s: multiple revisions locked by %s; please specify one", rcs->path, user); + return 1; + } + lock = p; + } + if (lock == NULL) + return 0; /* no lock found, ergo nothing to do */ + xrev = xstrdup (lock->key); + } + else if (RCS_nodeisbranch (rcs, rev)) + { + /* If rev is a branch number, unlock the latest revision on that + branch. */ + xrev = RCS_getbranch (rcs, (char *) rev, 1); + if (xrev == NULL) + { + error (0, 0, "%s: branch %s absent", rcs->path, rev); + return 1; + } + } + else + /* REV is an exact revision number. */ + xrev = xstrdup (rev); + + lock = findnode (RCS_getlocks (rcs), xrev); + if (lock == NULL) + { + /* This revision isn't locked. */ + free (xrev); + return 0; + } + + if (! STREQ (lock->data, user)) + { + /* If the revision is locked by someone else, notify + them. Note that this shouldn't ever happen if RCS_unlock + is called with a NULL revision, since that means "whatever + revision is currently locked by the caller." */ + char *repos, *workfile; + repos = xstrdup (rcs->path); + workfile = strrchr (repos, '/'); + *workfile++ = '\0'; + notify_do ('C', workfile, user, NULL, NULL, repos); + free (repos); + } + + delnode (lock); + if (!unlock_quiet) + { + cvs_output (xrev, 0); + cvs_output (" unlocked\n", 0); + } + + free (xrev); + return 0; +} + +/* Add USER to the access list of RCS. Do nothing if already present. + FIXME-twp: check syntax of USER to make sure it's a valid id. */ + +void +RCS_addaccess (rcs, user) + RCSNode *rcs; + char *user; +{ + char *access, *a; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rcs->access == NULL) + rcs->access = xstrdup (user); + else + { + access = xstrdup (rcs->access); + for (a = strtok (access, " "); a != NULL; a = strtok (NULL, " ")) + { + if (STREQ (a, user)) + { + free (access); + return; + } + } + rcs->access = (char *) xrealloc + (rcs->access, strlen (rcs->access) + strlen (user) + 2); + strcat (rcs->access, " "); + strcat (rcs->access, user); + } +} + +/* Remove USER from the access list of RCS. */ + +void +RCS_delaccess (rcs, user) + RCSNode *rcs; + char *user; +{ + char *p, *s; + int ulen; + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (rcs->access == NULL) + return; + + if (user == NULL) + { + free (rcs->access); + rcs->access = NULL; + return; + } + + p = rcs->access; + ulen = strlen (user); + while (p != NULL) + { + if (strncmp (p, user, ulen) == 0 && (p[ulen] == '\0' || p[ulen] == ' ')) + break; + p = strchr (p, ' '); + if (p != NULL) + ++p; + } + + if (p == NULL) + return; + + s = p + ulen; + while (*s != '\0') + *p++ = *s++; + *p = '\0'; +} + +char * +RCS_getaccess (rcs) + RCSNode *rcs; +{ + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + return rcs->access; +} + +static int findtag PROTO ((Node *, void *)); + +/* Return a nonzero value if the revision specified by ARG is found. */ + +static int +findtag (node, arg) + Node *node; + void *arg; +{ + char *rev = (char *)arg; + + if (STREQ (node->data, rev)) + return 1; + else + return 0; +} + +/* Delete revisions between REV1 and REV2. The changes between the two + revisions must be collapsed, and the result stored in the revision + immediately preceding the lower one. Return 0 for successful completion, + 1 otherwise. + + Solution: check out the revision preceding REV1 and the revision + following REV2. Use call_diff to find aggregate diffs between + these two revisions, and replace the delta text for the latter one + with the new aggregate diff. Alternatively, we could write a + function that takes two change texts and combines them to produce a + new change text, without checking out any revs or calling diff. It + would be hairy, but so, so cool. + + If INCLUSIVE is set, then TAG1 and TAG2, if non-NULL, tell us to + delete that revision as well (cvs admin -o tag1:tag2). If clear, + delete up to but not including that revision (cvs admin -o tag1::tag2). + This does not affect TAG1 or TAG2 being NULL; the meaning of the start + point in ::tag2 and :tag2 is the same and likewise for end points. */ + +int +RCS_delete_revs (rcs, tag1, tag2, inclusive) + RCSNode *rcs; + char *tag1; + char *tag2; + int inclusive; +{ + char *next; + Node *nodep; + RCSVers *revp = NULL; + RCSVers *beforep; + int status, found; + int save_noexec; + + char *branchpoint = NULL; + char *rev1 = NULL; + char *rev2 = NULL; + int rev1_inclusive = inclusive; + int rev2_inclusive = inclusive; + char *before = NULL; + char *after = NULL; + char *beforefile = NULL; + char *afterfile = NULL; + char *outfile = NULL; + + if (tag1 == NULL && tag2 == NULL) + return 0; + + /* Assume error status until everything is finished. */ + status = 1; + + /* Make sure both revisions exist. */ + if (tag1 != NULL) + { + rev1 = RCS_gettag (rcs, tag1, 1, NULL); + if (rev1 == NULL || (nodep = findnode (rcs->versions, rev1)) == NULL) + { + error (0, 0, "%s: Revision %s doesn't exist.", rcs->path, tag1); + goto delrev_done; + } + } + if (tag2 != NULL) + { + rev2 = RCS_gettag (rcs, tag2, 1, NULL); + if (rev2 == NULL || (nodep = findnode (rcs->versions, rev2)) == NULL) + { + error (0, 0, "%s: Revision %s doesn't exist.", rcs->path, tag2); + goto delrev_done; + } + } + + /* If rev1 is on the trunk and rev2 is NULL, rev2 should be + RCS->HEAD. (*Not* RCS_head(rcs), which may return rcs->branch + instead.) We need to check this special case early, in order + to make sure that rev1 and rev2 get ordered correctly. */ + if (rev2 == NULL && numdots (rev1) == 1) + { + rev2 = xstrdup (rcs->head); + rev2_inclusive = 1; + } + + if (rev2 == NULL) + rev2_inclusive = 1; + + if (rev1 != NULL && rev2 != NULL) + { + /* A range consisting of a branch number means the latest revision + on that branch. */ + if (RCS_isbranch (rcs, rev1) && STREQ (rev1, rev2)) + rev1 = rev2 = RCS_getbranch (rcs, rev1, 0); + else + { + /* Make sure REV1 and REV2 are ordered correctly (in the + same order as the next field). For revisions on the + trunk, REV1 should be higher than REV2; for branches, + REV1 should be lower. */ + /* Shouldn't we just be giving an error in the case where + the user specifies the revisions in the wrong order + (that is, always swap on the trunk, never swap on a + branch, in the non-error cases)? It is not at all + clear to me that users who specify -o 1.4:1.2 really + meant to type -o 1.2:1.4, and the out of order usage + has never been documented, either by cvs.texinfo or + rcs(1). */ + char *temp; + int temp_inclusive; + if (numdots (rev1) == 1) + { + if (compare_revnums (rev1, rev2) <= 0) + { + temp = rev2; + rev2 = rev1; + rev1 = temp; + + temp_inclusive = rev2_inclusive; + rev2_inclusive = rev1_inclusive; + rev1_inclusive = temp_inclusive; + } + } + else if (compare_revnums (rev1, rev2) > 0) + { + temp = rev2; + rev2 = rev1; + rev1 = temp; + + temp_inclusive = rev2_inclusive; + rev2_inclusive = rev1_inclusive; + rev1_inclusive = temp_inclusive; + } + } + } + + /* Basically the same thing; make sure that the ordering is what we + need. */ + if (rev1 == NULL) + { + assert (rev2 != NULL); + if (numdots (rev2) == 1) + { + /* Swap rev1 and rev2. */ + int temp_inclusive; + + rev1 = rev2; + rev2 = NULL; + + temp_inclusive = rev2_inclusive; + rev2_inclusive = rev1_inclusive; + rev1_inclusive = temp_inclusive; + } + } + + /* Put the revision number preceding the first one to delete into + BEFORE (where "preceding" means according to the next field). + If the first revision to delete is the first revision on its + branch (e.g. 1.3.2.1), BEFORE should be the node on the trunk + at which the branch is rooted. If the first revision to delete + is the head revision of the trunk, set BEFORE to NULL. + + Note that because BEFORE may not be on the same branch as REV1, + it is not very handy for navigating the revision tree. It's + most useful just for checking out the revision preceding REV1. */ + before = NULL; + branchpoint = RCS_getbranchpoint (rcs, rev1 != NULL ? rev1 : rev2); + if (rev1 == NULL) + { + rev1 = xstrdup (branchpoint); + if (numdots (branchpoint) > 1) + { + char *bp; + bp = strrchr (branchpoint, '.'); + while (*--bp != '.') + ; + *bp = '\0'; + /* Note that this is exclusive, always, because the inclusive + flag doesn't affect the meaning when rev1 == NULL. */ + before = xstrdup (branchpoint); + *bp = '.'; + } + } + else if (! STREQ (rev1, branchpoint)) + { + /* Walk deltas from BRANCHPOINT on, looking for REV1. */ + nodep = findnode (rcs->versions, branchpoint); + revp = (RCSVers *) nodep->data; + while (revp->next != NULL && ! STREQ (revp->next, rev1)) + { + revp = (RCSVers *) nodep->data; + nodep = findnode (rcs->versions, revp->next); + } + if (revp->next == NULL) + { + error (0, 0, "%s: Revision %s doesn't exist.", rcs->path, rev1); + goto delrev_done; + } + if (rev1_inclusive) + before = xstrdup (revp->version); + else + { + before = rev1; + nodep = findnode (rcs->versions, before); + rev1 = xstrdup (((RCSVers *)nodep->data)->next); + } + } + else if (!rev1_inclusive) + { + before = rev1; + nodep = findnode (rcs->versions, before); + rev1 = xstrdup (((RCSVers *)nodep->data)->next); + } + else if (numdots (branchpoint) > 1) + { + /* Example: rev1 is "1.3.2.1", branchpoint is "1.3.2.1". + Set before to "1.3". */ + char *bp; + bp = strrchr (branchpoint, '.'); + while (*--bp != '.') + ; + *bp = '\0'; + before = xstrdup (branchpoint); + *bp = '.'; + } + + /* If any revision between REV1 and REV2 is locked or is a branch point, + we can't delete that revision and must abort. */ + after = NULL; + next = rev1; + found = 0; + while (!found && next != NULL) + { + nodep = findnode (rcs->versions, next); + revp = (RCSVers *) nodep->data; + + if (rev2 != NULL) + found = STREQ (revp->version, rev2); + next = revp->next; + + if ((!found && next != NULL) || rev2_inclusive || rev2 == NULL) + { + if (findnode (RCS_getlocks (rcs), revp->version)) + { + error (0, 0, "%s: can't remove locked revision %s", + rcs->path, + revp->version); + goto delrev_done; + } + if (revp->branches != NULL) + { + error (0, 0, "%s: can't remove branch point %s", + rcs->path, + revp->version); + goto delrev_done; + } + + /* Doing this only for the :: syntax is for compatibility. + See cvs.texinfo for somewhat more discussion. */ + if (!inclusive + && walklist (RCS_symbols (rcs), findtag, revp->version)) + { + /* We don't print which file this happens to on the theory + that the caller will print the name of the file in a + more useful fashion (fullname not rcs->path). */ + error (0, 0, "cannot remove revision %s because it has tags", + revp->version); + goto delrev_done; + } + + /* It's misleading to print the `deleting revision' output + here, since we may not actually delete these revisions. + But that's how RCS does it. Bleah. Someday this should be + moved to the point where the revs are actually marked for + deletion. -twp */ + cvs_output ("deleting revision ", 0); + cvs_output (revp->version, 0); + cvs_output ("\n", 1); + } + } + + if (rev2 == NULL) + ; + else if (found) + { + if (rev2_inclusive) + after = xstrdup (next); + else + after = xstrdup (revp->version); + } + else if (!inclusive) + { + /* In the case of an empty range, for example 1.2::1.2 or + 1.2::1.3, we want to just do nothing. */ + status = 0; + goto delrev_done; + } + else + { + /* This looks fishy in the cases where tag1 == NULL or tag2 == NULL. + Are those cases really impossible? */ + assert (tag1 != NULL); + assert (tag2 != NULL); + + error (0, 0, "%s: invalid revision range %s:%s", rcs->path, + tag1, tag2); + goto delrev_done; + } + + if (after == NULL && before == NULL) + { + /* The user is trying to delete all revisions. While an + RCS file without revisions makes sense to RCS (e.g. the + state after "rcs -i"), CVS has never been able to cope with + it. So at least for now we just make this an error. + + We don't include rcs->path in the message since "cvs admin" + already printed "RCS file:" and the name. */ + error (1, 0, "attempt to delete all revisions"); + } + + /* The conditionals at this point get really hairy. Here is the + general idea: + + IF before != NULL and after == NULL + THEN don't check out any revisions, just delete them + IF before == NULL and after != NULL + THEN only check out after's revision, and use it for the new deltatext + ELSE + check out both revisions and diff -n them. This could use + RCS_exec_rcsdiff with some changes, like being able + to suppress diagnostic messages and to direct output. */ + + if (after != NULL) + { + char *diffbuf; + size_t bufsize, len; + +#if defined (__CYGWIN32__) || defined (_WIN32) + /* FIXME: This is an awful kludge, but at least until I have + time to work on it a little more and test it, I'd rather + give a fatal error than corrupt the file. I think that we + need to use "-kb" and "--binary" and "rb" to get_file + (probably can do it always, not just for binary files, if + we are consistent between the RCS_checkout and the diff). */ + { + char *expand = RCS_getexpand (rcs); + if (expand != NULL && STREQ (expand, "b")) + error (1, 0, + "admin -o not implemented yet for binary on this system"); + } +#endif + + afterfile = cvs_temp_name(); + status = RCS_checkout (rcs, NULL, after, NULL, "-ko", afterfile, + (RCSCHECKOUTPROC)0, NULL); + if (status > 0) + goto delrev_done; + + if (before == NULL) + { + /* We are deleting revisions from the head of the tree, + so must create a new head. */ + diffbuf = NULL; + bufsize = 0; + get_file (afterfile, afterfile, "r", &diffbuf, &bufsize, &len); + + save_noexec = noexec; + noexec = 0; + if (unlink_file (afterfile) < 0) + error (0, errno, "cannot remove %s", afterfile); + noexec = save_noexec; + + free (afterfile); + afterfile = NULL; + + free (rcs->head); + rcs->head = xstrdup (after); + } + else + { + beforefile = cvs_temp_name(); + status = RCS_checkout (rcs, NULL, before, NULL, "-ko", beforefile, + (RCSCHECKOUTPROC)0, NULL); + if (status > 0) + goto delrev_done; + + outfile = cvs_temp_name(); + status = diff_exec (beforefile, afterfile, "-an", outfile); + + if (status == 2) + { + /* Not sure we need this message; will diff_exec already + have printed an error? */ + error (0, 0, "%s: could not diff", rcs->path); + status = 1; + goto delrev_done; + } + + diffbuf = NULL; + bufsize = 0; + get_file (outfile, outfile, "r", &diffbuf, &bufsize, &len); + } + + /* Save the new change text in after's delta node. */ + nodep = findnode (rcs->versions, after); + revp = (RCSVers *) nodep->data; + + assert (revp->text == NULL); + + revp->text = (Deltatext *) xmalloc (sizeof (Deltatext)); + memset ((Deltatext *) revp->text, 0, sizeof (Deltatext)); + revp->text->version = xstrdup (revp->version); + revp->text->text = diffbuf; + revp->text->len = len; + + /* If DIFFBUF is NULL, it means that OUTFILE is empty and that + there are no differences between the two revisions. In that + case, we want to force RCS_copydeltas to write an empty string + for the new change text (leaving the text field set NULL + means "preserve the original change text for this delta," so + we don't want that). */ + if (revp->text->text == NULL) + revp->text->text = xstrdup (""); + } + + /* Walk through the revisions (again) to mark each one as + outdated. (FIXME: would it be safe to use the `dead' field for + this? Doubtful.) */ + for (next = rev1; + next != NULL && (after == NULL || ! STREQ (next, after)); + next = revp->next) + { + nodep = findnode (rcs->versions, next); + revp = (RCSVers *) nodep->data; + revp->outdated = 1; + } + + /* Update delta links. If BEFORE == NULL, we're changing the + head of the tree and don't need to update any `next' links. */ + if (before != NULL) + { + /* If REV1 is the first node on its branch, then BEFORE is its + root node (on the trunk) and we have to update its branches + list. Otherwise, BEFORE is on the same branch as AFTER, and + we can just change BEFORE's `next' field to point to AFTER. + (This should be safe: since findnode manages its lists via + the `hashnext' and `hashprev' fields, rather than `next' and + `prev', mucking with `next' and `prev' should not corrupt the + delta tree's internal structure. Much. -twp) */ + + if (rev1 == NULL) + /* beforep's ->next field already should be equal to after, + which I think is always NULL in this case. */ + ; + else if (STREQ (rev1, branchpoint)) + { + nodep = findnode (rcs->versions, before); + revp = (RCSVers *) nodep->data; + nodep = revp->branches->list->next; + while (nodep != revp->branches->list && + ! STREQ (nodep->key, rev1)) + nodep = nodep->next; + assert (nodep != revp->branches->list); + if (after == NULL) + delnode (nodep); + else + { + free (nodep->key); + nodep->key = xstrdup (after); + } + } + else + { + nodep = findnode (rcs->versions, before); + beforep = (RCSVers *) nodep->data; + free (beforep->next); + beforep->next = xstrdup (after); + } + } + + status = 0; + + delrev_done: + if (rev1 != NULL) + free (rev1); + if (rev2 != NULL) + free (rev2); + if (branchpoint != NULL) + free (branchpoint); + if (before != NULL) + free (before); + if (after != NULL) + free (after); + + save_noexec = noexec; + noexec = 0; + if (beforefile != NULL) + { + if (unlink_file (beforefile) < 0) + error (0, errno, "cannot remove %s", beforefile); + free (beforefile); + } + if (afterfile != NULL) + { + if (unlink_file (afterfile) < 0) + error (0, errno, "cannot remove %s", afterfile); + free (afterfile); + } + if (outfile != NULL) + { + if (unlink_file (outfile) < 0) + error (0, errno, "cannot remove %s", outfile); + free (outfile); + } + noexec = save_noexec; + + return status; +} + +/* + * TRUE if there exists a symbolic tag "tag" in file. + */ +int +RCS_exist_tag (rcs, tag) + RCSNode *rcs; + char *tag; +{ + + assert (rcs != NULL); + + if (findnode (RCS_symbols (rcs), tag)) + return 1; + return 0; + +} + +/* + * TRUE if RCS revision number "rev" exists. + * This includes magic branch revisions, not found in rcs->versions, + * but only in rcs->symbols, requiring a list walk to find them. + * Take advantage of list walk callback function already used by + * RCS_delete_revs, above. + */ +int +RCS_exist_rev (rcs, rev) + RCSNode *rcs; + char *rev; +{ + + assert (rcs != NULL); + + if (rcs->flags & PARTIAL) + RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL); + + if (findnode(rcs->versions, rev) != 0) + return 1; + + if (walklist (RCS_symbols(rcs), findtag, rev) != 0) + return 1; + + return 0; + +} + + +/* RCS_deltas and friends. Processing of the deltas in RCS files. */ + +struct line +{ + /* Text of this line. Part of the same malloc'd block as the struct + line itself (we probably should use the "struct hack" (char text[1]) + and save ourselves sizeof (char *) bytes). Does not include \n; + instead has_newline indicates the presence or absence of \n. */ + char *text; + /* Length of this line, not counting \n if has_newline is true. */ + size_t len; + /* Version in which it was introduced. */ + RCSVers *vers; + /* Nonzero if this line ends with \n. This will always be true + except possibly for the last line. */ + int has_newline; + /* Number of pointers to this struct line. */ + int refcount; +}; + +struct linevector +{ + /* How many lines in use for this linevector? */ + unsigned int nlines; + /* How many lines allocated for this linevector? */ + unsigned int lines_alloced; + /* Pointer to array containing a pointer to each line. */ + struct line **vector; +}; + +static void linevector_init PROTO ((struct linevector *)); + +/* Initialize *VEC to be a linevector with no lines. */ +static void +linevector_init (vec) + struct linevector *vec; +{ + vec->lines_alloced = 0; + vec->nlines = 0; + vec->vector = NULL; +} + +static int linevector_add PROTO ((struct linevector *vec, const char *text, + size_t len, RCSVers *vers, + unsigned int pos)); + +/* Given some text TEXT, add each of its lines to VEC before line POS + (where line 0 is the first line). The last line in TEXT may or may + not be \n terminated. + Set the version for each of the new lines to VERS. This + function returns non-zero for success. It returns zero if the line + number is out of range. + + Each of the lines in TEXT are copied to space which is managed with + the linevector (and freed by linevector_free). So the caller doesn't + need to keep TEXT around after the call to this function. */ +static int +linevector_add (vec, text, len, vers, pos) + struct linevector *vec; + const char *text; + size_t len; + RCSVers *vers; + unsigned int pos; +{ + const char *textend; + unsigned int i; + unsigned int nnew; + const char *p; + const char *nextline_text; + size_t nextline_len; + int nextline_newline; + struct line *q; + + if (len == 0) + return 1; + + textend = text + len; + + /* Count the number of lines we will need to add. */ + nnew = 1; + for (p = text; p < textend; ++p) + if (*p == '\n' && p + 1 < textend) + ++nnew; + + /* Expand VEC->VECTOR if needed. */ + if (vec->nlines + nnew >= vec->lines_alloced) + { + if (vec->lines_alloced == 0) + vec->lines_alloced = 10; + while (vec->nlines + nnew >= vec->lines_alloced) + vec->lines_alloced *= 2; + vec->vector = xrealloc (vec->vector, + vec->lines_alloced * sizeof (*vec->vector)); + } + + /* Make room for the new lines in VEC->VECTOR. */ + for (i = vec->nlines + nnew - 1; i >= pos + nnew; --i) + vec->vector[i] = vec->vector[i - nnew]; + + if (pos > vec->nlines) + return 0; + + /* Actually add the lines, to VEC->VECTOR. */ + i = pos; + nextline_text = text; + nextline_newline = 0; + for (p = text; p < textend; ++p) + if (*p == '\n') + { + nextline_newline = 1; + if (p + 1 == textend) + /* If there are no characters beyond the last newline, we + don't consider it another line. */ + break; + nextline_len = p - nextline_text; + q = (struct line *) xmalloc (sizeof (struct line) + nextline_len); + q->vers = vers; + q->text = (char *)q + sizeof (struct line); + q->len = nextline_len; + q->has_newline = nextline_newline; + q->refcount = 1; + memcpy (q->text, nextline_text, nextline_len); + vec->vector[i++] = q; + + nextline_text = (char *)p + 1; + nextline_newline = 0; + } + nextline_len = p - nextline_text; + q = (struct line *) xmalloc (sizeof (struct line) + nextline_len); + q->vers = vers; + q->text = (char *)q + sizeof (struct line); + q->len = nextline_len; + q->has_newline = nextline_newline; + q->refcount = 1; + memcpy (q->text, nextline_text, nextline_len); + vec->vector[i] = q; + + vec->nlines += nnew; + + return 1; +} + +static void linevector_delete PROTO ((struct linevector *, unsigned int, + unsigned int)); + +/* Remove NLINES lines from VEC at position POS (where line 0 is the + first line). */ +static void +linevector_delete (vec, pos, nlines) + struct linevector *vec; + unsigned int pos; + unsigned int nlines; +{ + unsigned int i; + unsigned int last; + + last = vec->nlines - nlines; + for (i = pos; i < pos + nlines; ++i) + { + if (--vec->vector[i]->refcount == 0) + free (vec->vector[i]); + } + for (i = pos; i < last; ++i) + vec->vector[i] = vec->vector[i + nlines]; + vec->nlines -= nlines; +} + +static void linevector_copy PROTO ((struct linevector *, struct linevector *)); + +/* Copy FROM to TO, copying the vectors but not the lines pointed to. */ +static void +linevector_copy (to, from) + struct linevector *to; + struct linevector *from; +{ + unsigned int ln; + + for (ln = 0; ln < to->nlines; ++ln) + { + if (--to->vector[ln]->refcount == 0) + free (to->vector[ln]); + } + if (from->nlines > to->lines_alloced) + { + if (to->lines_alloced == 0) + to->lines_alloced = 10; + while (from->nlines > to->lines_alloced) + to->lines_alloced *= 2; + to->vector = (struct line **) + xrealloc (to->vector, to->lines_alloced * sizeof (*to->vector)); + } + memcpy (to->vector, from->vector, + from->nlines * sizeof (*to->vector)); + to->nlines = from->nlines; + for (ln = 0; ln < to->nlines; ++ln) + ++to->vector[ln]->refcount; +} + +static void linevector_free PROTO ((struct linevector *)); + +/* Free storage associated with linevector. */ +static void +linevector_free (vec) + struct linevector *vec; +{ + unsigned int ln; + + if (vec->vector != NULL) + { + for (ln = 0; ln < vec->nlines; ++ln) + if (--vec->vector[ln]->refcount == 0) + free (vec->vector[ln]); + + free (vec->vector); + } +} + +static char *month_printname PROTO ((char *)); + +/* Given a textual string giving the month (1-12), terminated with any + character not recognized by atoi, return the 3 character name to + print it with. I do not think it is a good idea to change these + strings based on the locale; they are standard abbreviations (for + example in rfc822 mail messages) which should be widely understood. + Returns a pointer into static readonly storage. */ +static char * +month_printname (month) + char *month; +{ + static const char *const months[] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + int mnum; + + mnum = atoi (month); + if (mnum < 1 || mnum > 12) + return "???"; + return (char *)months[mnum - 1]; +} + +static int +apply_rcs_changes PROTO ((struct linevector *, const char *, size_t, + const char *, RCSVers *, RCSVers *)); + +/* Apply changes to the line vector LINES. DIFFBUF is a buffer of + length DIFFLEN holding the change text from an RCS file (the output + of diff -n). NAME is used in error messages. The VERS field of + any line added is set to ADDVERS. The VERS field of any line + deleted is set to DELVERS, unless DELVERS is NULL, in which case + the VERS field of deleted lines is unchanged. The function returns + non-zero if the change text is applied successfully. It returns + zero if the change text does not appear to apply to LINES (e.g., a + line number is invalid). If the change text is improperly + formatted (e.g., it is not the output of diff -n), the function + calls error with a status of 1, causing the program to exit. */ + +static int +apply_rcs_changes (lines, diffbuf, difflen, name, addvers, delvers) + struct linevector *lines; + const char *diffbuf; + size_t difflen; + const char *name; + RCSVers *addvers; + RCSVers *delvers; +{ + const char *p; + const char *q; + int op; + /* The RCS format throws us for a loop in that the deltafrags (if + we define a deltafrag as an add or a delete) need to be applied + in reverse order. So we stick them into a linked list. */ + struct deltafrag { + enum {FRAG_ADD, FRAG_DELETE} type; + unsigned long pos; + unsigned long nlines; + const char *new_lines; + size_t len; + struct deltafrag *next; + }; + struct deltafrag *dfhead; + struct deltafrag *df; + + dfhead = NULL; + for (p = diffbuf; p != NULL && p < diffbuf + difflen; ) + { + op = *p++; + if (op != 'a' && op != 'd') + /* Can't just skip over the deltafrag, because the value + of op determines the syntax. */ + error (1, 0, "unrecognized operation '\\x%x' in %s", + op, name); + df = (struct deltafrag *) xmalloc (sizeof (struct deltafrag)); + df->next = dfhead; + dfhead = df; + df->pos = strtoul (p, (char **) &q, 10); + + if (p == q) + error (1, 0, "number expected in %s", name); + p = q; + if (*p++ != ' ') + error (1, 0, "space expected in %s", name); + df->nlines = strtoul (p, (char **) &q, 10); + if (p == q) + error (1, 0, "number expected in %s", name); + p = q; + if (*p++ != '\012') + error (1, 0, "linefeed expected in %s", name); + + if (op == 'a') + { + unsigned int i; + + df->type = FRAG_ADD; + i = df->nlines; + /* The text we want is the number of lines specified, or + until the end of the value, whichever comes first (it + will be the former except in the case where we are + adding a line which does not end in newline). */ + for (q = p; i != 0; ++q) + if (*q == '\n') + --i; + else if (q == diffbuf + difflen) + { + if (i != 1) + error (1, 0, "premature end of change in %s", name); + else + break; + } + + /* Stash away a pointer to the text we are adding. */ + df->new_lines = p; + df->len = q - p; + + p = q; + } + else + { + /* Correct for the fact that line numbers in RCS files + start with 1. */ + --df->pos; + + assert (op == 'd'); + df->type = FRAG_DELETE; + } + } + + for (df = dfhead; df != NULL;) + { + unsigned int ln; + + switch (df->type) + { + case FRAG_ADD: + if (! linevector_add (lines, df->new_lines, df->len, addvers, + df->pos)) + return 0; + break; + case FRAG_DELETE: + if (df->pos > lines->nlines + || df->pos + df->nlines > lines->nlines) + return 0; + if (delvers != NULL) + for (ln = df->pos; ln < df->pos + df->nlines; ++ln) + lines->vector[ln]->vers = delvers; + linevector_delete (lines, df->pos, df->nlines); + break; + } + df = df->next; + free (dfhead); + dfhead = df; + } + + return 1; +} + +/* Apply an RCS change text to a buffer. The function name starts + with rcs rather than RCS because this does not take an RCSNode + argument. NAME is used in error messages. TEXTBUF is the text + buffer to change, and TEXTLEN is the size. DIFFBUF and DIFFLEN are + the change buffer and size. The new buffer is returned in *RETBUF + and *RETLEN. The new buffer is allocated by xmalloc. + + Return 1 for success. On failure, call error and return 0. */ + +int +rcs_change_text (name, textbuf, textlen, diffbuf, difflen, retbuf, retlen) + const char *name; + char *textbuf; + size_t textlen; + const char *diffbuf; + size_t difflen; + char **retbuf; + size_t *retlen; +{ + struct linevector lines; + int ret; + + *retbuf = NULL; + *retlen = 0; + + linevector_init (&lines); + + if (! linevector_add (&lines, textbuf, textlen, NULL, 0)) + error (1, 0, "cannot initialize line vector"); + + if (! apply_rcs_changes (&lines, diffbuf, difflen, name, NULL, NULL)) + { + error (0, 0, "invalid change text in %s", name); + ret = 0; + } + else + { + char *p; + size_t n; + unsigned int ln; + + n = 0; + for (ln = 0; ln < lines.nlines; ++ln) + /* 1 for \n */ + n += lines.vector[ln]->len + 1; + + p = xmalloc (n); + *retbuf = p; + + for (ln = 0; ln < lines.nlines; ++ln) + { + memcpy (p, lines.vector[ln]->text, lines.vector[ln]->len); + p += lines.vector[ln]->len; + if (lines.vector[ln]->has_newline) + *p++ = '\n'; + } + + *retlen = p - *retbuf; + assert (*retlen <= n); + + ret = 1; + } + + linevector_free (&lines); + + return ret; +} + +/* Walk the deltas in RCS to get to revision VERSION. + + If OP is RCS_ANNOTATE, then write annotations using cvs_output. + + If OP is RCS_FETCH, then put the contents of VERSION into a + newly-malloc'd array and put a pointer to it in *TEXT. Each line + is \n terminated; the caller is responsible for converting text + files if desired. The total length is put in *LEN. + + If FP is non-NULL, it should be a file descriptor open to the file + RCS with file position pointing to the deltas. We close the file + when we are done. + + If LOG is non-NULL, then *LOG is set to the log message of VERSION, + and *LOGLEN is set to the length of the log message. + + On error, give a fatal error. */ + +static void +RCS_deltas (rcs, fp, rcsbuf, version, op, text, len, log, loglen) + RCSNode *rcs; + FILE *fp; + struct rcsbuffer *rcsbuf; + char *version; + enum rcs_delta_op op; + char **text; + size_t *len; + char **log; + size_t *loglen; +{ + struct rcsbuffer rcsbuf_local; + char *branchversion; + char *cpversion; + char *key; + char *value; + size_t vallen; + RCSVers *vers; + RCSVers *prev_vers; + RCSVers *trunk_vers; + char *next; + int ishead, isnext, isversion, onbranch; + Node *node; + struct linevector headlines; + struct linevector curlines; + struct linevector trunklines; + int foundhead; + + if (fp == NULL) + { + rcsbuf_cache_open (rcs, rcs->delta_pos, &fp, &rcsbuf_local); + rcsbuf = &rcsbuf_local; + } + + ishead = 1; + vers = NULL; + prev_vers = NULL; + trunk_vers = NULL; + next = NULL; + onbranch = 0; + foundhead = 0; + + linevector_init (&curlines); + linevector_init (&headlines); + linevector_init (&trunklines); + + /* We set BRANCHVERSION to the version we are currently looking + for. Initially, this is the version on the trunk from which + VERSION branches off. If VERSION is not a branch, then + BRANCHVERSION is just VERSION. */ + branchversion = xstrdup (version); + cpversion = strchr (branchversion, '.'); + if (cpversion != NULL) + cpversion = strchr (cpversion + 1, '.'); + if (cpversion != NULL) + *cpversion = '\0'; + + do { + if (! rcsbuf_getrevnum (rcsbuf, &key)) + error (1, 0, "unexpected EOF reading RCS file %s", rcs->path); + + if (next != NULL && ! STREQ (next, key)) + { + /* This is not the next version we need. It is a branch + version which we want to ignore. */ + isnext = 0; + isversion = 0; + } + else + { + isnext = 1; + + /* look up the revision */ + node = findnode (rcs->versions, key); + if (node == NULL) + error (1, 0, + "mismatch in rcs file %s between deltas and deltatexts", + rcs->path); + + /* Stash the previous version. */ + prev_vers = vers; + + vers = (RCSVers *) node->data; + next = vers->next; + + /* Compare key and trunkversion now, because key points to + storage controlled by rcsbuf_getkey. */ + if (STREQ (branchversion, key)) + isversion = 1; + else + isversion = 0; + } + + while (1) + { + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + error (1, 0, "%s does not appear to be a valid rcs file", + rcs->path); + + if (log != NULL + && isversion + && STREQ (key, "log") + && STREQ (branchversion, version)) + { + *log = rcsbuf_valcopy (rcsbuf, value, 0, loglen); + } + + if (STREQ (key, "text")) + { + rcsbuf_valpolish (rcsbuf, value, 0, &vallen); + if (ishead) + { + if (! linevector_add (&curlines, value, vallen, NULL, 0)) + error (1, 0, "invalid rcs file %s", rcs->path); + + ishead = 0; + } + else if (isnext) + { + if (! apply_rcs_changes (&curlines, value, vallen, + rcs->path, + onbranch ? vers : NULL, + onbranch ? NULL : prev_vers)) + error (1, 0, "invalid change text in %s", rcs->path); + } + break; + } + } + + if (isversion) + { + /* This is either the version we want, or it is the + branchpoint to the version we want. */ + if (STREQ (branchversion, version)) + { + /* This is the version we want. */ + linevector_copy (&headlines, &curlines); + foundhead = 1; + if (onbranch) + { + /* We have found this version by tracking up a + branch. Restore back to the lines we saved + when we left the trunk, and continue tracking + down the trunk. */ + onbranch = 0; + vers = trunk_vers; + next = vers->next; + linevector_copy (&curlines, &trunklines); + } + } + else + { + Node *p; + + /* We need to look up the branch. */ + onbranch = 1; + + if (numdots (branchversion) < 2) + { + unsigned int ln; + + /* We are leaving the trunk; save the current + lines so that we can restore them when we + continue tracking down the trunk. */ + trunk_vers = vers; + linevector_copy (&trunklines, &curlines); + + /* Reset the version information we have + accumulated so far. It only applies to the + changes from the head to this version. */ + for (ln = 0; ln < curlines.nlines; ++ln) + curlines.vector[ln]->vers = NULL; + } + + /* The next version we want is the entry on + VERS->branches which matches this branch. For + example, suppose VERSION is 1.21.4.3 and + BRANCHVERSION was 1.21. Then we look for an entry + starting with "1.21.4" and we'll put it (probably + 1.21.4.1) in NEXT. We'll advance BRANCHVERSION by + two dots (in this example, to 1.21.4.3). */ + + if (vers->branches == NULL) + error (1, 0, "missing expected branches in %s", + rcs->path); + *cpversion = '.'; + ++cpversion; + cpversion = strchr (cpversion, '.'); + if (cpversion == NULL) + error (1, 0, "version number confusion in %s", + rcs->path); + for (p = vers->branches->list->next; + p != vers->branches->list; + p = p->next) + if (strncmp (p->key, branchversion, + cpversion - branchversion) == 0) + break; + if (p == vers->branches->list) + error (1, 0, "missing expected branch in %s", + rcs->path); + + next = p->key; + + cpversion = strchr (cpversion + 1, '.'); + if (cpversion != NULL) + *cpversion = '\0'; + } + } + if (op == RCS_FETCH && foundhead) + break; + } while (next != NULL); + + free (branchversion); + + rcsbuf_cache (rcs, rcsbuf); + + if (! foundhead) + error (1, 0, "could not find desired version %s in %s", + version, rcs->path); + + /* Now print out or return the data we have just computed. */ + switch (op) + { + case RCS_ANNOTATE: + { + unsigned int ln; + + for (ln = 0; ln < headlines.nlines; ++ln) + { + char buf[80]; + /* Period which separates year from month in date. */ + char *ym; + /* Period which separates month from day in date. */ + char *md; + RCSVers *prvers; + + prvers = headlines.vector[ln]->vers; + if (prvers == NULL) + prvers = vers; + + sprintf (buf, "%-12s (%-8.8s ", + prvers->version, + prvers->author); + cvs_output (buf, 0); + + /* Now output the date. */ + ym = strchr (prvers->date, '.'); + if (ym == NULL) + { + /* ??- is an ANSI trigraph. The ANSI way to + avoid it is \? but some pre ANSI compilers + complain about the unrecognized escape + sequence. Of course string concatenation + ("??" "-???") is also an ANSI-ism. Testing + __STDC__ seems to be a can of worms, since + compilers do all kinds of things with it. */ + cvs_output ("??", 0); + cvs_output ("-???", 0); + cvs_output ("-??", 0); + } + else + { + md = strchr (ym + 1, '.'); + if (md == NULL) + cvs_output ("??", 0); + else + cvs_output (md + 1, 2); + + cvs_output ("-", 1); + cvs_output (month_printname (ym + 1), 0); + cvs_output ("-", 1); + /* Only output the last two digits of the year. Our output + lines are long enough as it is without printing the + century. */ + cvs_output (ym - 2, 2); + } + cvs_output ("): ", 0); + if (headlines.vector[ln]->len != 0) + cvs_output (headlines.vector[ln]->text, + headlines.vector[ln]->len); + cvs_output ("\n", 1); + } + } + break; + case RCS_FETCH: + { + char *p; + size_t n; + unsigned int ln; + + assert (text != NULL); + assert (len != NULL); + + n = 0; + for (ln = 0; ln < headlines.nlines; ++ln) + /* 1 for \n */ + n += headlines.vector[ln]->len + 1; + p = xmalloc (n); + *text = p; + for (ln = 0; ln < headlines.nlines; ++ln) + { + memcpy (p, headlines.vector[ln]->text, + headlines.vector[ln]->len); + p += headlines.vector[ln]->len; + if (headlines.vector[ln]->has_newline) + *p++ = '\n'; + } + *len = p - *text; + assert (*len <= n); + } + break; + } + + linevector_free (&curlines); + linevector_free (&headlines); + linevector_free (&trunklines); + + return; +} + +/* Read the information for a single delta from the RCS buffer RCSBUF, + whose name is RCSFILE. *KEYP and *VALP are either NULL, or the + first key/value pair to read, as set by rcsbuf_getkey. Return NULL + if there are no more deltas. Store the key/value pair which + terminated the read in *KEYP and *VALP. */ + +static RCSVers * +getdelta (rcsbuf, rcsfile, keyp, valp) + struct rcsbuffer *rcsbuf; + char *rcsfile; + char **keyp; + char **valp; +{ + RCSVers *vnode; + char *key, *value, *keybuf, *valbuf, *cp; + Node *kv; + + /* Get revision number if it wasn't passed in. This uses + rcsbuf_getkey because it doesn't croak when encountering + unexpected input. As a result, we have to play unholy games + with `key' and `value'. */ + if (*keyp != NULL) + { + key = *keyp; + value = *valp; + } + else + { + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + error (1, 0, "%s: unexpected EOF", rcsfile); + } + + /* Make sure that it is a revision number and not a cabbage + or something. */ + for (cp = key; + (isdigit ((unsigned char) *cp) || *cp == '.') && *cp != '\0'; + cp++) + /* do nothing */ ; + /* Note that when comparing with RCSDATE, we are not massaging + VALUE from the string found in the RCS file. This is OK since + we know exactly what to expect. */ + if (*cp != '\0' || strncmp (RCSDATE, value, (sizeof RCSDATE) - 1) != 0) + { + *keyp = key; + *valp = value; + return NULL; + } + + vnode = (RCSVers *) xmalloc (sizeof (RCSVers)); + memset (vnode, 0, sizeof (RCSVers)); + + vnode->version = xstrdup (key); + + /* Grab the value of the date from value. Note that we are not + massaging VALUE from the string found in the RCS file. */ + cp = value + (sizeof RCSDATE) - 1; /* skip the "date" keyword */ + while (whitespace (*cp)) /* take space off front of value */ + cp++; + + vnode->date = xstrdup (cp); + + /* Get author field. */ + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + { + error (1, 0, "unexpected end of file reading %s", rcsfile); + } + if (! STREQ (key, "author")) + error (1, 0, "\ +unable to parse %s; `author' not in the expected place", rcsfile); + vnode->author = rcsbuf_valcopy (rcsbuf, value, 0, (size_t *) NULL); + + /* Get state field. */ + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + { + error (1, 0, "unexpected end of file reading %s", rcsfile); + } + if (! STREQ (key, "state")) + error (1, 0, "\ +unable to parse %s; `state' not in the expected place", rcsfile); + vnode->state = rcsbuf_valcopy (rcsbuf, value, 0, (size_t *) NULL); + /* The value is optional, according to rcsfile(5). */ + if (value != NULL && STREQ (value, "dead")) + { + vnode->dead = 1; + } + + /* Note that "branches" and "next" are in fact mandatory, according + to doc/RCSFILES. */ + + /* fill in the branch list (if any branches exist) */ + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + { + error (1, 0, "unexpected end of file reading %s", rcsfile); + } + if (STREQ (key, RCSDESC)) + { + *keyp = key; + *valp = value; + /* Probably could/should be a fatal error. */ + error (0, 0, "warning: 'branches' keyword missing from %s", rcsfile); + return vnode; + } + if (value != (char *) NULL) + { + vnode->branches = getlist (); + /* Note that we are not massaging VALUE from the string found + in the RCS file. */ + do_branches (vnode->branches, value); + } + + /* fill in the next field if there is a next revision */ + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + { + error (1, 0, "unexpected end of file reading %s", rcsfile); + } + if (STREQ (key, RCSDESC)) + { + *keyp = key; + *valp = value; + /* Probably could/should be a fatal error. */ + error (0, 0, "warning: 'next' keyword missing from %s", rcsfile); + return vnode; + } + if (value != (char *) NULL) + vnode->next = rcsbuf_valcopy (rcsbuf, value, 0, (size_t *) NULL); + + /* + * XXX - this is where we put the symbolic link stuff??? + * (into newphrases in the deltas). + */ + while (1) + { + int len; + size_t valbuflen; + + key = NULL; + + if (! rcsbuf_getid (rcsbuf, &keybuf)) + error (1, 0, "unexpected end of file reading %s", rcsfile); + + /* rcsbuf_getid did not terminate the key, so copy it to new space. */ + len = rcsbuf->ptr - keybuf; + key = (char *) xmalloc (len + 1); + strncpy (key, keybuf, len); + key[len] = '\0'; + + /* The `desc' keyword has only a single string value, with no + trailing semicolon, so it must be handled specially. */ + if (STREQ (key, RCSDESC)) + { + (void) rcsbuf_getstring (rcsbuf, &valbuf); + value = rcsbuf_valcopy (rcsbuf, valbuf, 1, &valbuflen); + break; + } + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* The `hardlinks' value is a group of words, which must + be parsed separately and added as a list to vnode->hardlinks. */ + if (STREQ (key, "hardlinks")) + { + Node *n; + + vnode->hardlinks = getlist(); + while (1) + { + if (! rcsbuf_getword (rcsbuf, &valbuf)) + error (1, 0, "unexpected end of file reading %s", rcsfile); + if (valbuf == NULL) + break; + n = getnode(); + n->key = rcsbuf_valcopy (rcsbuf, valbuf, 1, NULL); + addnode (vnode->hardlinks, n); + } + continue; + } +#endif + + /* Get the value. */ + value = NULL; + while (1) + { + if (! rcsbuf_getword (rcsbuf, &valbuf)) + error (1, 0, "unexpected end of file reading %s", rcsfile); + if (valbuf == NULL) + break; + + /* Copy valbuf to new space so we can polish it, then + append it to value. */ + + if (value == NULL) + { + value = rcsbuf_valcopy (rcsbuf, valbuf, 1, &valbuflen); + } + else + { + char *temp_value; + + temp_value = rcsbuf_valcopy (rcsbuf, valbuf, 1, &valbuflen); + len = strlen (value); + value = (char *) xrealloc (value, len + valbuflen + 2); + value[len] = ' '; + strcpy (value + len + 1, temp_value); + free (temp_value); + } + } + + /* Enable use of repositories created by certain obsolete + versions of CVS. This code should remain indefinately; + there is no procedure for converting old repositories, and + checking for it is harmless. */ + if (STREQ (key, RCSDEAD)) + { + vnode->dead = 1; + if (vnode->state != NULL) + free (vnode->state); + vnode->state = xstrdup ("dead"); + continue; + } + /* if we have a new revision number, we're done with this delta */ + for (cp = key; + (isdigit ((unsigned char) *cp) || *cp == '.') && *cp != '\0'; + cp++) + /* do nothing */ ; + /* Note that when comparing with RCSDATE, we are not massaging + VALUE from the string found in the RCS file. This is OK + since we know exactly what to expect. */ + if (*cp == '\0' && strncmp (RCSDATE, value, strlen (RCSDATE)) == 0) + break; + + /* At this point, key and value represent a user-defined field + in the delta node. */ + if (vnode->other_delta == NULL) + vnode->other_delta = getlist (); + kv = getnode (); + kv->type = RCSFIELD; + kv->key = key; + kv->data = value; + if (addnode (vnode->other_delta, kv) != 0) + { + /* Complaining about duplicate keys in newphrases seems + questionable, in that we don't know what they mean and + doc/RCSFILES has no prohibition on several newphrases + with the same key. But we can't store more than one as + long as we store them in a List *. */ + error (0, 0, "warning: duplicate key `%s' in RCS file `%s'", + key, rcsfile); + freenode (kv); + } + } + + /* Return the key which caused us to fail back to the caller. */ + *keyp = key; + *valp = value; + + return vnode; +} + +static void +freedeltatext (d) + Deltatext *d; +{ + if (d->version != NULL) + free (d->version); + if (d->log != NULL) + free (d->log); + if (d->text != NULL) + free (d->text); + if (d->other != (List *) NULL) + dellist (&d->other); + free (d); +} + +static Deltatext * +RCS_getdeltatext (rcs, fp, rcsbuf) + RCSNode *rcs; + FILE *fp; + struct rcsbuffer *rcsbuf; +{ + char *num; + char *key, *value; + Node *p; + Deltatext *d; + + /* Get the revision number. */ + if (! rcsbuf_getrevnum (rcsbuf, &num)) + { + /* If num == NULL, it means we reached EOF naturally. That's + fine. */ + if (num == NULL) + return NULL; + else + error (1, 0, "%s: unexpected EOF", rcs->path); + } + + p = findnode (rcs->versions, num); + if (p == NULL) + error (1, 0, "mismatch in rcs file %s between deltas and deltatexts", + rcs->path); + + d = (Deltatext *) xmalloc (sizeof (Deltatext)); + d->version = xstrdup (num); + + /* Get the log message. */ + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + error (1, 0, "%s, delta %s: unexpected EOF", rcs->path, num); + if (! STREQ (key, "log")) + error (1, 0, "%s, delta %s: expected `log', got `%s'", + rcs->path, num, key); + d->log = rcsbuf_valcopy (rcsbuf, value, 0, (size_t *) NULL); + + /* Get random newphrases. */ + d->other = getlist(); + while (1) + { + if (! rcsbuf_getkey (rcsbuf, &key, &value)) + error (1, 0, "%s, delta %s: unexpected EOF", rcs->path, num); + + if (STREQ (key, "text")) + break; + + p = getnode(); + p->type = RCSFIELD; + p->key = xstrdup (key); + p->data = rcsbuf_valcopy (rcsbuf, value, 1, (size_t *) NULL); + if (addnode (d->other, p) < 0) + { + error (0, 0, "warning: %s, delta %s: duplicate field `%s'", + rcs->path, num, key); + } + } + + /* Get the change text. We already know that this key is `text'. */ + d->text = rcsbuf_valcopy (rcsbuf, value, 0, &d->len); + + return d; +} + +/* RCS output functions, for writing RCS format files from RCSNode + structures. + + For most of this work, RCS 5.7 uses an `aprintf' function which aborts + program upon error. Instead, these functions check the output status + of the stream right before closing it, and aborts if an error condition + is found. The RCS solution is probably the better one: it produces + more overhead, but will produce a clearer diagnostic in the case of + catastrophic error. In either case, however, the repository will probably + not get corrupted. */ + +static int +putsymbol_proc (symnode, fparg) + Node *symnode; + void *fparg; +{ + FILE *fp = (FILE *) fparg; + + /* A fiddly optimization: this code used to just call fprintf, but + in an old repository with hundreds of tags this can get called + hundreds of thousands of times when doing a cvs tag. Since + tagging is a relatively common operation, and using putc and + fputs is just as comprehensible, the change is worthwhile. */ + putc ('\n', fp); + putc ('\t', fp); + fputs (symnode->key, fp); + putc (':', fp); + fputs (symnode->data, fp); + return 0; +} + +static int putlock_proc PROTO ((Node *, void *)); + +/* putlock_proc is like putsymbol_proc, but key and data are reversed. */ + +static int +putlock_proc (symnode, fp) + Node *symnode; + void *fp; +{ + return fprintf ((FILE *) fp, "\n\t%s:%s", symnode->data, symnode->key); +} + +static int +putrcsfield_proc (node, vfp) + Node *node; + void *vfp; +{ + FILE *fp = (FILE *) vfp; + + /* Some magic keys used internally by CVS start with `;'. Skip them. */ + if (node->key[0] == ';') + return 0; + + fprintf (fp, "\n%s\t", node->key); + if (node->data != NULL) + { + /* If the field's value contains evil characters, + it must be stringified. */ + /* FIXME: This does not quite get it right. "7jk8f" is not a legal + value for a value in a newpharse, according to doc/RCSFILES, + because digits are not valid in an "id". We might do OK by + always writing strings (enclosed in @@). Would be nice to + explicitly mention this one way or another in doc/RCSFILES. + A case where we are wrong in a much more clear-cut way is that + we let through non-graphic characters such as whitespace and + control characters. */ + int n = strcspn (node->data, "$,.:;@"); + if (node->data[n] == 0) + fputs (node->data, fp); + else + { + putc ('@', fp); + expand_at_signs (node->data, (off_t) strlen (node->data), fp); + putc ('@', fp); + } + } + + /* desc, log and text fields should not be terminated with semicolon; + all other fields should be. */ + if (! STREQ (node->key, "desc") && + ! STREQ (node->key, "log") && + ! STREQ (node->key, "text")) + { + putc (';', fp); + } + return 0; +} + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + +/* Save a filename in a `hardlinks' RCS field. NODE->KEY will contain + a full pathname, but currently only basenames are stored in the RCS + node. Assume that the filename includes nasty characters and + @-escape it. */ + +static int +puthardlink_proc (node, vfp) + Node *node; + void *vfp; +{ + FILE *fp = (FILE *) vfp; + char *basename = strrchr (node->key, '/'); + + if (basename == NULL) + basename = node->key; + else + ++basename; + + putc ('\t', fp); + putc ('@', fp); + (void) expand_at_signs (basename, strlen (basename), fp); + putc ('@', fp); + + return 0; +} + +#endif + +/* Output the admin node for RCS into stream FP. */ + +static void +RCS_putadmin (rcs, fp) + RCSNode *rcs; + FILE *fp; +{ + fprintf (fp, "%s\t%s;\n", RCSHEAD, rcs->head ? rcs->head : ""); + if (rcs->branch) + fprintf (fp, "%s\t%s;\n", RCSBRANCH, rcs->branch); + + fputs ("access", fp); + if (rcs->access) + { + char *p, *s; + s = xstrdup (rcs->access); + for (p = strtok (s, " \n\t"); p != NULL; p = strtok (NULL, " \n\t")) + fprintf (fp, "\n\t%s", p); + free (s); + } + fputs (";\n", fp); + + fputs (RCSSYMBOLS, fp); + /* If we haven't had to convert the symbols to a list yet, don't + force a conversion now; just write out the string. */ + if (rcs->symbols == NULL && rcs->symbols_data != NULL) + { + fputs ("\n\t", fp); + fputs (rcs->symbols_data, fp); + } + else + walklist (RCS_symbols (rcs), putsymbol_proc, (void *) fp); + fputs (";\n", fp); + + fputs ("locks", fp); + if (rcs->locks_data) + fprintf (fp, "\t%s", rcs->locks_data); + else if (rcs->locks) + walklist (rcs->locks, putlock_proc, (void *) fp); + if (rcs->strict_locks) + fprintf (fp, "; strict"); + fputs (";\n", fp); + + if (rcs->comment) + { + fprintf (fp, "comment\t@"); + expand_at_signs (rcs->comment, (off_t) strlen (rcs->comment), fp); + fputs ("@;\n", fp); + } + if (rcs->expand && ! STREQ (rcs->expand, "kv")) + fprintf (fp, "%s\t@%s@;\n", RCSEXPAND, rcs->expand); + + walklist (rcs->other, putrcsfield_proc, (void *) fp); + + putc ('\n', fp); +} + +static void +putdelta (vers, fp) + RCSVers *vers; + FILE *fp; +{ + Node *bp, *start; + + /* Skip if no revision was supplied, or if it is outdated (cvs admin -o) */ + if (vers == NULL || vers->outdated) + return; + + fprintf (fp, "\n%s\n%s\t%s;\t%s %s;\t%s %s;\nbranches", + vers->version, + RCSDATE, vers->date, + "author", vers->author, + "state", vers->state ? vers->state : ""); + + if (vers->branches != NULL) + { + start = vers->branches->list; + for (bp = start->next; bp != start; bp = bp->next) + fprintf (fp, "\n\t%s", bp->key); + } + + fprintf (fp, ";\nnext\t%s;", vers->next ? vers->next : ""); + + walklist (vers->other_delta, putrcsfield_proc, fp); + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (vers->hardlinks) + { + fprintf (fp, "\nhardlinks"); + walklist (vers->hardlinks, puthardlink_proc, fp); + putc (';', fp); + } +#endif + putc ('\n', fp); +} + +static void +RCS_putdtree (rcs, rev, fp) + RCSNode *rcs; + char *rev; + FILE *fp; +{ + RCSVers *versp; + Node *p, *branch; + + if (rev == NULL) + return; + + /* Find the delta node for this revision. */ + p = findnode (rcs->versions, rev); + if(p == NULL){ + /* This shouldn't happen if the repository is in good shape. + We need to clean up the lockfile, but we can't call + rcs_internal_unlockfile, because that assumes that things + are behaving normally. rcs_internal_unlockfile copies the + lockfile back onto the repository (,v) file. + Since the repository file is corrupt, this isn't a terribly + destructive thing to do, but just in case the user isn't + maintaining backups, and the ,v file does contain useful + information, we'll just delete the lockfile and let the + user sort out the problem. */ + + if (fclose(fp) == EOF) + error (1, 0, "error closing lock file %s", rcs_lockfile); + unlink_file(rcs_lockfile); + error (1, 0, + "error parsing repository file %s, file may be corrupt.", + rcs->path); + } + + versp = (RCSVers *) p->data; + + /* Print the delta node and recurse on its `next' node. This prints + the trunk. If there are any branches printed on this revision, + print those trunks as well. */ + putdelta (versp, fp); + RCS_putdtree (rcs, versp->next, fp); + if (versp->branches != NULL) + { + branch = versp->branches->list; + for (p = branch->next; p != branch; p = p->next) + RCS_putdtree (rcs, p->key, fp); + } +} + +static void +RCS_putdesc (rcs, fp) + RCSNode *rcs; + FILE *fp; +{ + fprintf (fp, "\n\n%s\n@", RCSDESC); + if (rcs->desc != NULL) + { + off_t len = (off_t) strlen (rcs->desc); + if (len > 0) + { + expand_at_signs (rcs->desc, len, fp); + if (rcs->desc[len-1] != '\n') + putc ('\n', fp); + } + } + fputs ("@\n", fp); +} + +static void +putdeltatext (fp, d) + FILE *fp; + Deltatext *d; +{ + fprintf (fp, "\n\n%s\nlog\n@", d->version); + if (d->log != NULL) + { + int loglen = strlen (d->log); + expand_at_signs (d->log, (off_t) loglen, fp); + if (d->log[loglen-1] != '\n') + putc ('\n', fp); + } + putc ('@', fp); + + walklist (d->other, putrcsfield_proc, fp); + + fputs ("\ntext\n@", fp); + if (d->text != NULL) + expand_at_signs (d->text, (off_t) d->len, fp); + fputs ("@\n", fp); +} + +/* TODO: the whole mechanism for updating deltas is kludgey... more + sensible would be to supply all the necessary info in a `newdeltatext' + field for RCSVers nodes. -twp */ + +/* Copy delta text nodes from FIN to FOUT. If NEWDTEXT is non-NULL, it + is a new delta text node, and should be added to the tree at the + node whose revision number is INSERTPT. (Note that trunk nodes are + written in decreasing order, and branch nodes are written in + increasing order.) */ + +static void +RCS_copydeltas (rcs, fin, rcsbufin, fout, newdtext, insertpt) + RCSNode *rcs; + FILE *fin; + struct rcsbuffer *rcsbufin; + FILE *fout; + Deltatext *newdtext; + char *insertpt; +{ + int actions; + RCSVers *dadmin; + Node *np; + int insertbefore, found; + char *bufrest; + int nls; + size_t buflen; + char buf[8192]; + int got; + + /* Count the number of versions for which we have to do some + special operation. */ + actions = walklist (rcs->versions, count_delta_actions, (void *) NULL); + + /* Make a note of whether NEWDTEXT should be inserted + before or after its INSERTPT. */ + insertbefore = (newdtext != NULL && numdots (newdtext->version) == 1); + + while (actions != 0 || newdtext != NULL) + { + Deltatext *dtext; + + dtext = RCS_getdeltatext (rcs, fin, rcsbufin); + + /* We shouldn't hit EOF here, because that would imply that + some action was not taken, or that we could not insert + NEWDTEXT. */ + if (dtext == NULL) + error (1, 0, "internal error: EOF too early in RCS_copydeltas"); + + found = (insertpt != NULL && STREQ (dtext->version, insertpt)); + if (found && insertbefore) + { + putdeltatext (fout, newdtext); + newdtext = NULL; + insertpt = NULL; + } + + np = findnode (rcs->versions, dtext->version); + dadmin = (RCSVers *) np->data; + + /* If this revision has been outdated, just skip it. */ + if (dadmin->outdated) + { + --actions; + continue; + } + + /* Update the change text for this delta. New change text + data may come from cvs admin -m, cvs admin -o, or cvs ci. */ + if (dadmin->text != NULL) + { + if (dadmin->text->log != NULL || dadmin->text->text != NULL) + --actions; + if (dadmin->text->log != NULL) + { + free (dtext->log); + dtext->log = dadmin->text->log; + dadmin->text->log = NULL; + } + if (dadmin->text->text != NULL) + { + free (dtext->text); + dtext->text = dadmin->text->text; + dtext->len = dadmin->text->len; + dadmin->text->text = NULL; + } + } + putdeltatext (fout, dtext); + freedeltatext (dtext); + + if (found && !insertbefore) + { + putdeltatext (fout, newdtext); + newdtext = NULL; + insertpt = NULL; + } + } + + /* Copy the rest of the file directly, without bothering to + interpret it. The caller will handle error checking by calling + ferror. + + We just wrote a newline to the file, either in putdeltatext or + in the caller. However, we may not have read the corresponding + newline from the file, because rcsbuf_getkey returns as soon as + it finds the end of the '@' string for the desc or text key. + Therefore, we may read three newlines when we should really + only write two, and we check for that case here. This is not + an semantically important issue; we only do it to make our RCS + files look traditional. */ + + nls = 3; + + rcsbuf_get_buffered (rcsbufin, &bufrest, &buflen); + if (buflen > 0) + { + if (bufrest[0] != '\n' + || strncmp (bufrest, "\n\n\n", buflen < 3 ? buflen : 3) != 0) + { + nls = 0; + } + else + { + if (buflen < 3) + nls -= buflen; + else + { + ++bufrest; + --buflen; + nls = 0; + } + } + + fwrite (bufrest, 1, buflen, fout); + } + + while ((got = fread (buf, 1, sizeof buf, fin)) != 0) + { + if (nls > 0 + && got >= nls + && buf[0] == '\n' + && strncmp (buf, "\n\n\n", nls) == 0) + { + fwrite (buf + 1, 1, got - 1, fout); + } + else + { + fwrite (buf, 1, got, fout); + } + + nls = 0; + } +} + +/* A helper procedure for RCS_copydeltas. This is called via walklist + to count the number of RCS revisions for which some special action + is required. */ + +static int +count_delta_actions (np, ignore) + Node *np; + void *ignore; +{ + RCSVers *dadmin; + + dadmin = (RCSVers *) np->data; + + if (dadmin->outdated) + return 1; + + if (dadmin->text != NULL + && (dadmin->text->log != NULL || dadmin->text->text != NULL)) + { + return 1; + } + + return 0; +} + +/* + * Clean up temporary files + */ +static RETSIGTYPE +rcs_cleanup () +{ + /* Note that the checks for existence_error are because we are + called from a signal handler, so we don't know whether the + files got created. */ + + /* FIXME: Do not perform buffered I/O from an interrupt handler like + this (via error). However, I'm leaving the error-calling code there + in the hope that on the rare occasion the error call is actually made + (e.g., a fluky I/O error or permissions problem prevents the deletion + of a just-created file) reentrancy won't be an issue. */ + if (rcs_lockfile != NULL) + { + if (unlink_file (rcs_lockfile) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", rcs_lockfile); + } + rcs_lockfile = NULL; +} + +/* RCS_internal_lockfile and RCS_internal_unlockfile perform RCS-style + locking on the specified RCSFILE: for a file called `foo,v', open + for writing a file called `,foo,'. + + Note that we what do here is quite different from what RCS does. + RCS creates the ,foo, file before it reads the RCS file (if it + knows that it will be writing later), so that it actually serves as + a lock. We don't; instead we rely on CVS writelocks. This means + that if someone is running RCS on the file at the same time they + are running CVS on it, they might lose (we read the file, + then RCS writes it, then we write it, clobbering the + changes made by RCS). I believe the current sentiment about this + is "well, don't do that". + + A concern has been expressed about whether adopting the RCS + strategy would slow us down. I don't think so, since we need to + write the ,foo, file anyway (unless perhaps if O_EXCL is slower or + something). + + These do not perform quite the same function as the RCS -l option + for locking files: they are intended to prevent competing RCS + processes from stomping all over each other's laundry. Hence, + they are `internal' locking functions. + + If there is an error, give a fatal error; if we return we always + return a non-NULL value. */ + +static FILE * +rcs_internal_lockfile (rcsfile) + char *rcsfile; +{ + int fd; + struct stat rstat; + FILE *fp; + static int first_call = 1; + + if (first_call) + { + first_call = 0; + /* clean up if we get a signal */ +#ifdef SIGHUP + (void) SIG_register (SIGHUP, rcs_cleanup); +#endif +#ifdef SIGINT + (void) SIG_register (SIGINT, rcs_cleanup); +#endif +#ifdef SIGQUIT + (void) SIG_register (SIGQUIT, rcs_cleanup); +#endif +#ifdef SIGPIPE + (void) SIG_register (SIGPIPE, rcs_cleanup); +#endif +#ifdef SIGTERM + (void) SIG_register (SIGTERM, rcs_cleanup); +#endif + } + + /* Get the lock file name: `,file,' for RCS file `file,v'. */ + assert (rcs_lockfile == NULL); + rcs_lockfile = rcs_lockfilename (rcsfile); + + /* Use the existing RCS file mode, or read-only if this is a new + file. (Really, this is a lie -- if this is a new file, + RCS_checkin uses the permissions from the working copy. For + actually creating the file, we use 0444 as a safe default mode.) */ + if (stat (rcsfile, &rstat) < 0) + { + if (existence_error (errno)) + rstat.st_mode = S_IRUSR | S_IRGRP | S_IROTH; + else + error (1, errno, "cannot stat %s", rcsfile); + } + + /* Try to open exclusively. POSIX.1 guarantees that O_EXCL|O_CREAT + guarantees an exclusive open. According to the RCS source, with + NFS v2 we must also throw in O_TRUNC and use an open mask that makes + the file unwriteable. For extensive justification, see the comments for + rcswriteopen() in rcsedit.c, in RCS 5.7. This is kind of pointless + in the CVS case; see comment at the start of this file concerning + general ,foo, file strategy. + + There is some sentiment that with NFSv3 and such, that one can + rely on O_EXCL these days. This might be true for unix (I + don't really know), but I am still pretty skeptical in the case + of the non-unix systems. */ + fd = open (rcs_lockfile, + OPEN_BINARY | O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, + S_IRUSR | S_IRGRP | S_IROTH); + + if (fd < 0) + { + error (1, errno, "could not open lock file `%s'", rcs_lockfile); + } + + /* Force the file permissions, and return a stream object. */ + /* Because we change the modes later, we don't worry about + this in the non-HAVE_FCHMOD case. */ +#ifdef HAVE_FCHMOD + if (fchmod (fd, rstat.st_mode) < 0) + error (1, errno, "cannot change mode for %s", rcs_lockfile); +#endif + fp = fdopen (fd, FOPEN_BINARY_WRITE); + if (fp == NULL) + error (1, errno, "cannot fdopen %s", rcs_lockfile); + + return fp; +} + +static void +rcs_internal_unlockfile (fp, rcsfile) + FILE *fp; + char *rcsfile; +{ + assert (rcs_lockfile != NULL); + + /* Abort if we could not write everything successfully to LOCKFILE. + This is not a great error-handling mechanism, but should prevent + corrupting the repository. */ + + if (ferror (fp)) + /* The only case in which using errno here would be meaningful + is if we happen to have left errno unmolested since the call + which produced the error (e.g. fprintf). That is pretty + fragile even if it happens to sometimes be true. The real + solution is to check each call to fprintf rather than waiting + until the end like this. */ + error (1, 0, "error writing to lock file %s", rcs_lockfile); + if (fclose (fp) == EOF) + error (1, errno, "error closing lock file %s", rcs_lockfile); + + rename_file (rcs_lockfile, rcsfile); + + { + /* Use a temporary to make sure there's no interval + (after rcs_lockfile has been freed but before it's set to NULL) + during which the signal handler's use of rcs_lockfile would + reference freed memory. */ + char *tmp = rcs_lockfile; + rcs_lockfile = NULL; + free (tmp); + } +} + +static char * +rcs_lockfilename (rcsfile) + char *rcsfile; +{ + char *lockfile, *lockp; + char *rcsbase, *rcsp, *rcsend; + int rcslen; + + /* Create the lockfile name. */ + rcslen = strlen (rcsfile); + lockfile = (char *) xmalloc (rcslen + 10); + rcsbase = last_component (rcsfile); + rcsend = rcsfile + rcslen - sizeof(RCSEXT); + for (lockp = lockfile, rcsp = rcsfile; rcsp < rcsbase; ++rcsp) + *lockp++ = *rcsp; + *lockp++ = ','; + while (rcsp <= rcsend) + *lockp++ = *rcsp++; + *lockp++ = ','; + *lockp = '\0'; + + return lockfile; +} + +/* Rewrite an RCS file. The basic idea here is that the caller should + first call RCS_reparsercsfile, then munge the data structures as + desired (via RCS_delete_revs, RCS_settag, &c), then call RCS_rewrite. */ + +void +RCS_rewrite (rcs, newdtext, insertpt) + RCSNode *rcs; + Deltatext *newdtext; + char *insertpt; +{ + FILE *fin, *fout; + struct rcsbuffer rcsbufin; + + if (noexec) + return; + + /* Make sure we're operating on an actual file and not a symlink. */ + resolve_symlink (&(rcs->path)); + + fout = rcs_internal_lockfile (rcs->path); + + RCS_putadmin (rcs, fout); + RCS_putdtree (rcs, rcs->head, fout); + RCS_putdesc (rcs, fout); + + /* Open the original RCS file and seek to the first delta text. */ + rcsbuf_cache_open (rcs, rcs->delta_pos, &fin, &rcsbufin); + + /* Update delta_pos to the current position in the output file. + Do NOT move these statements: they must be done after fin has + been positioned at the old delta_pos, but before any delta + texts have been written to fout. */ + rcs->delta_pos = ftell (fout); + if (rcs->delta_pos == -1) + error (1, errno, "cannot ftell in RCS file %s", rcs->path); + + RCS_copydeltas (rcs, fin, &rcsbufin, fout, newdtext, insertpt); + + /* We don't want to call rcsbuf_cache here, since we're about to + delete the file. */ + rcsbuf_close (&rcsbufin); + if (ferror (fin)) + /* The only case in which using errno here would be meaningful + is if we happen to have left errno unmolested since the call + which produced the error (e.g. fread). That is pretty + fragile even if it happens to sometimes be true. The real + solution is to make sure that all the code which reads + from fin checks for errors itself (some does, some doesn't). */ + error (0, 0, "warning: when closing RCS file `%s'", rcs->path); + if (fclose (fin) < 0) + error (0, errno, "warning: closing RCS file `%s'", rcs->path); + + rcs_internal_unlockfile (fout, rcs->path); +} + + +/* Annotate command. In rcs.c for historical reasons (from back when + what is now RCS_deltas was part of annotate_fileproc). */ + +/* Options from the command line. */ + +static int force_tag_match = 1; +static char *tag = NULL; +static char *date = NULL; + +static int annotate_fileproc PROTO ((void *callerdat, struct file_info *)); + +static int +annotate_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + FILE *fp = NULL; + struct rcsbuffer *rcsbufp = NULL; + struct rcsbuffer rcsbuf; + char *version; + + if (finfo->rcs == NULL) + return (1); + + if (finfo->rcs->flags & PARTIAL) + { + RCS_reparsercsfile (finfo->rcs, &fp, &rcsbuf); + rcsbufp = &rcsbuf; + } + + version = RCS_getversion (finfo->rcs, tag, date, force_tag_match, + (int *) NULL); + if (version == NULL) + return 0; + + /* Distinguish output for various files if we are processing + several files. */ + cvs_outerr ("Annotations for ", 0); + cvs_outerr (finfo->fullname, 0); + cvs_outerr ("\n***************\n", 0); + + RCS_deltas (finfo->rcs, fp, rcsbufp, version, RCS_ANNOTATE, NULL, + NULL, NULL, NULL); + free (version); + return 0; +} + +static const char *const annotate_usage[] = +{ + "Usage: %s %s [-lRf] [-r rev|-D date] [files...]\n", + "\t-l\tLocal directory only, no recursion.\n", + "\t-R\tProcess directories recursively.\n", + "\t-f\tUse head revision if tag/date not found.\n", + "\t-r rev\tAnnotate file as of specified revision/tag.\n", + "\t-D date\tAnnotate file as of specified date.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* Command to show the revision, date, and author where each line of a + file was modified. */ + +int +annotate (argc, argv) + int argc; + char **argv; +{ + int local = 0; + int c; + + if (argc == -1) + usage (annotate_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+lr:D:fR")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'r': + tag = optarg; + break; + case 'D': + date = Make_Date (optarg); + break; + case 'f': + force_tag_match = 0; + break; + case '?': + default: + usage (annotate_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + ign_setup (); + + if (local) + send_arg ("-l"); + if (!force_tag_match) + send_arg ("-f"); + option_with_arg ("-r", tag); + if (date) + client_senddate (date); + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server ("annotate\012", 0); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + if (tag != NULL) + tag_check_valid (tag, argc, argv, local, 0, ""); + + return start_recursion (annotate_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 1, (char *)NULL, + 1); +} + +/* + * For a given file with full pathname PATH and revision number REV, + * produce a file label suitable for passing to diff. The default + * file label as used by RCS 5.7 looks like this: + * + * FILENAME <tab> YYYY/MM/DD <sp> HH:MM:SS <tab> REVNUM + * + * The date and time used are the revision's last checkin date and time. + * If REV is NULL, use the working copy's mtime instead. + */ +char * +make_file_label (path, rev, rcs) + char *path; + char *rev; + RCSNode *rcs; +{ + char datebuf[MAXDATELEN]; + char *label; + char *file; + + file = last_component (path); + label = (char *) xmalloc (strlen (path) + + (rev == NULL ? 0 : strlen (rev)) + + 50); + + if (rev) + { + char *date; + RCS_getrevtime (rcs, rev, datebuf, 0); + date = printable_date (datebuf); + (void) sprintf (label, "-L%s\t%s\t%s", path, date, rev); + free (date); + } + else + { + struct stat sb; + struct tm *wm; + + if (CVS_STAT (file, &sb) < 0) + error (0, 1, "could not get info for `%s'", path); + else + { + wm = gmtime (&sb.st_mtime); + (void) sprintf (datebuf, "%04d/%02d/%02d %02d:%02d:%02d", + wm->tm_year + 1900, wm->tm_mon + 1, + wm->tm_mday, wm->tm_hour, + wm->tm_min, wm->tm_sec); + (void) sprintf (label, "-L%s\t%s", path, datebuf); + } + } + return label; +} diff --git a/gnu/dist/cvs/src/rcscmds.c b/gnu/dist/cvs/src/rcscmds.c new file mode 100644 index 00000000000..ab94e407bd7 --- /dev/null +++ b/gnu/dist/cvs/src/rcscmds.c @@ -0,0 +1,667 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * The functions in this file provide an interface for performing + * operations directly on RCS files. + */ + +#include "cvs.h" +#include <assert.h> +#include <stdio.h> +#include "diffrun.h" + +/* This file, rcs.h, and rcs.c, together sometimes known as the "RCS + library", are intended to define our interface to RCS files. + + Whether there will also be a version of RCS which uses this + library, or whether the library will be packaged for uses beyond + CVS or RCS (many people would like such a thing) is an open + question. Some considerations: + + 1. An RCS library for CVS must have the capabilities of the + existing CVS code which accesses RCS files. In particular, simple + approaches will often be slow. + + 2. An RCS library should not use code from the current RCS + (5.7 and its ancestors). The code has many problems. Too few + comments, too many layers of abstraction, too many global variables + (the correct number for a library is zero), too much intricately + interwoven functionality, and too many clever hacks. Paul Eggert, + the current RCS maintainer, agrees. + + 3. More work needs to be done in terms of separating out the RCS + library from the rest of CVS (for example, cvs_output should be + replaced by a callback, and the declarations should be centralized + into rcs.h, and probably other such cleanups). + + 4. To be useful for RCS and perhaps for other uses, the library + may need features beyond those needed by CVS. + + 5. Any changes to the RCS file format *must* be compatible. Many, + many tools (not just CVS and RCS) can at least import this format. + RCS and CVS must preserve the current ability to import/export it + (preferably improved--magic branches are currently a roadblock). + See doc/RCSFILES in the CVS distribution for documentation of this + file format. + + On a related note, see the comments at diff_exec, later in this file, + for more on the diff library. */ + +static void RCS_output_diff_options PROTO ((char *, char *, char *, char *)); + + +/* Stuff to deal with passing arguments the way libdiff.a wants to deal + with them. This is a crufty interface; there is no good reason for it + to resemble a command line rather than something closer to "struct + log_data" in log.c. */ + +/* First call call_diff_setup to setup any initial arguments. The + argument will be parsed into whitespace separated words and added + to the global call_diff_argv list. + + Then, optionally, call call_diff_arg for each additional argument + that you'd like to pass to the diff library. + + Finally, call call_diff or call_diff3 to produce the diffs. */ + +static char **call_diff_argv; +static int call_diff_argc; +static int call_diff_argc_allocated; + +static void call_diff_add_arg PROTO ((const char *)); +static void call_diff_setup PROTO ((const char *prog)); +static int call_diff PROTO ((char *out)); +static int call_diff3 PROTO ((char *out)); + +static void call_diff_write_output PROTO((const char *, size_t)); +static void call_diff_flush_output PROTO((void)); +static void call_diff_write_stdout PROTO((const char *)); +static void call_diff_error PROTO((const char *, const char *, const char *)); + +/* VARARGS */ +static void +call_diff_setup (prog) + const char *prog; +{ + char *cp; + int i; + char *call_diff_prog; + + /* clean out any malloc'ed values from call_diff_argv */ + for (i = 0; i < call_diff_argc; i++) + { + if (call_diff_argv[i]) + { + free (call_diff_argv[i]); + call_diff_argv[i] = (char *) 0; + } + } + call_diff_argc = 0; + + call_diff_prog = xstrdup (prog); + + /* put each word into call_diff_argv, allocating it as we go */ + for (cp = strtok (call_diff_prog, " \t"); + cp != NULL; + cp = strtok ((char *) NULL, " \t")) + call_diff_add_arg (cp); + free (call_diff_prog); +} + +static void +call_diff_arg (s) + const char *s; +{ + call_diff_add_arg (s); +} + +static void +call_diff_add_arg (s) + const char *s; +{ + /* allocate more argv entries if we've run out */ + if (call_diff_argc >= call_diff_argc_allocated) + { + call_diff_argc_allocated += 50; + call_diff_argv = (char **) + xrealloc ((char *) call_diff_argv, + call_diff_argc_allocated * sizeof (char **)); + } + + if (s) + call_diff_argv[call_diff_argc++] = xstrdup (s); + else + /* Not post-incremented on purpose! */ + call_diff_argv[call_diff_argc] = (char *) 0; +} + +/* Callback function for the diff library to write data to the output + file. This is used when we are producing output to stdout. */ + +static void +call_diff_write_output (text, len) + const char *text; + size_t len; +{ + if (len > 0) + cvs_output (text, len); +} + +/* Call back function for the diff library to flush the output file. + This is used when we are producing output to stdout. */ + +static void +call_diff_flush_output () +{ + cvs_flushout (); +} + +/* Call back function for the diff library to write to stdout. */ + +static void +call_diff_write_stdout (text) + const char *text; +{ + cvs_output (text, 0); +} + +/* Call back function for the diff library to write to stderr. */ + +static void +call_diff_error (format, a1, a2) + const char *format; + const char *a1; + const char *a2; +{ + /* FIXME: Should we somehow indicate that this error is coming from + the diff library? */ + error (0, 0, format, a1, a2); +} + +/* This set of callback functions is used if we are sending the diff + to stdout. */ + +static struct diff_callbacks call_diff_stdout_callbacks = +{ + call_diff_write_output, + call_diff_flush_output, + call_diff_write_stdout, + call_diff_error +}; + +/* This set of callback functions is used if we are sending the diff + to a file. */ + +static struct diff_callbacks call_diff_file_callbacks = +{ + (void (*) PROTO((const char *, size_t))) NULL, + (void (*) PROTO((void))) NULL, + call_diff_write_stdout, + call_diff_error +}; + +static int +call_diff (out) + char *out; +{ + if (out == RUN_TTY) + return diff_run (call_diff_argc, call_diff_argv, NULL, + &call_diff_stdout_callbacks); + else + return diff_run (call_diff_argc, call_diff_argv, out, + &call_diff_file_callbacks); +} + +static int +call_diff3 (out) + char *out; +{ + if (out == RUN_TTY) + return diff3_run (call_diff_argc, call_diff_argv, NULL, + &call_diff_stdout_callbacks); + else + return diff3_run (call_diff_argc, call_diff_argv, out, + &call_diff_file_callbacks); +} + + + +/* Merge revisions REV1 and REV2. */ + +int +RCS_merge(rcs, path, workfile, options, rev1, rev2) + RCSNode *rcs; + char *path; + char *workfile; + char *options; + char *rev1; + char *rev2; +{ + char *xrev1, *xrev2; + char *tmp1, *tmp2; + char *diffout = NULL; + int retval; + + if (options != NULL && options[0] != '\0') + assert (options[0] == '-' && options[1] == 'k'); + + cvs_output ("RCS file: ", 0); + cvs_output (rcs->path, 0); + cvs_output ("\n", 1); + + /* Calculate numeric revision numbers from rev1 and rev2 (may be + symbolic). */ + xrev1 = RCS_gettag (rcs, rev1, 0, NULL); + xrev2 = RCS_gettag (rcs, rev2, 0, NULL); + + /* Check out chosen revisions. The error message when RCS_checkout + fails is not very informative -- it is taken verbatim from RCS 5.7, + and relies on RCS_checkout saying something intelligent upon failure. */ + cvs_output ("retrieving revision ", 0); + cvs_output (xrev1, 0); + cvs_output ("\n", 1); + + tmp1 = cvs_temp_name(); + if (RCS_checkout (rcs, NULL, xrev1, rev1, options, tmp1, + (RCSCHECKOUTPROC)0, NULL)) + { + cvs_outerr ("rcsmerge: co failed\n", 0); + error_exit(); + } + + cvs_output ("retrieving revision ", 0); + cvs_output (xrev2, 0); + cvs_output ("\n", 1); + + tmp2 = cvs_temp_name(); + if (RCS_checkout (rcs, NULL, xrev2, rev2, options, tmp2, + (RCSCHECKOUTPROC)0, NULL)) + { + cvs_outerr ("rcsmerge: co failed\n", 0); + error_exit(); + } + + /* Merge changes. */ + cvs_output ("Merging differences between ", 0); + cvs_output (xrev1, 0); + cvs_output (" and ", 0); + cvs_output (xrev2, 0); + cvs_output (" into ", 0); + cvs_output (workfile, 0); + cvs_output ("\n", 1); + + /* Remember that the first word in the `call_diff_setup' string is used now + only for diagnostic messages -- CVS no longer forks to run diff3. */ + diffout = cvs_temp_name(); + call_diff_setup ("diff3"); + call_diff_arg ("-E"); + call_diff_arg ("-am"); + + call_diff_arg ("-L"); + call_diff_arg (workfile); + call_diff_arg ("-L"); + call_diff_arg (xrev1); + call_diff_arg ("-L"); + call_diff_arg (xrev2); + + call_diff_arg (workfile); + call_diff_arg (tmp1); + call_diff_arg (tmp2); + + retval = call_diff3 (diffout); + + if (retval == 1) + cvs_outerr ("rcsmerge: warning: conflicts during merge\n", 0); + else if (retval == 2) + error_exit(); + + if (diffout) + copy_file (diffout, workfile); + + /* Clean up. */ + { + int save_noexec = noexec; + noexec = 0; + if (unlink_file (tmp1) < 0) + { + if (!existence_error (errno)) + error (0, errno, "cannot remove temp file %s", tmp1); + } + free (tmp1); + if (unlink_file (tmp2) < 0) + { + if (!existence_error (errno)) + error (0, errno, "cannot remove temp file %s", tmp2); + } + free (tmp2); + if (diffout) + { + if (unlink_file (diffout) < 0) + { + if (!existence_error (errno)) + error (0, errno, "cannot remove temp file %s", diffout); + } + free (diffout); + } + free (xrev1); + free (xrev2); + noexec = save_noexec; + } + + return retval; +} + +/* Diff revisions and/or files. OPTS controls the format of the diff + (it contains options such as "-w -c", &c), or "" for the default. + OPTIONS controls keyword expansion, as a string starting with "-k", + or "" to use the default. REV1 is the first revision to compare + against; it must be non-NULL. If REV2 is non-NULL, compare REV1 + and REV2; if REV2 is NULL compare REV1 with the file in the working + directory, whose name is WORKFILE. LABEL1 and LABEL2 are default + file labels, and (if non-NULL) should be added as -L options + to diff. Output goes to stdout. + + Return value is 0 for success, -1 for a failure which set errno, + or positive for a failure which printed a message on stderr. + + This used to exec rcsdiff, but now calls RCS_checkout and diff_exec. + + An issue is what timezone is used for the dates which appear in the + diff output. rcsdiff uses the -z flag, which is not presently + processed by CVS diff, but I'm not sure exactly how hard to worry + about this--any such features are undocumented in the context of + CVS, and I'm not sure how important to users. */ +int +RCS_exec_rcsdiff (rcsfile, opts, options, rev1, rev2, label1, label2, workfile) + RCSNode *rcsfile; + char *opts; + char *options; + char *rev1; + char *rev2; + char *label1; + char *label2; + char *workfile; +{ + char *tmpfile1; + char *tmpfile2; + char *use_file2; + int status, retval; + + tmpfile1 = cvs_temp_name (); + tmpfile2 = NULL; + + cvs_output ("\ +===================================================================\n\ +RCS file: ", 0); + cvs_output (rcsfile->path, 0); + cvs_output ("\n", 1); + + /* Historically, `cvs diff' has expanded the $Name keyword to the + empty string when checking out revisions. This is an accident, + but no one has considered the issue thoroughly enough to determine + what the best behavior is. Passing NULL for the `nametag' argument + preserves the existing behavior. */ + + cvs_output ("retrieving revision ", 0); + cvs_output (rev1, 0); + cvs_output ("\n", 1); + status = RCS_checkout (rcsfile, NULL, rev1, NULL, options, tmpfile1, + (RCSCHECKOUTPROC)0, NULL); + if (status > 0) + { + retval = status; + goto error_return; + } + else if (status < 0) + { + error (0, errno, + "cannot check out revision %s of %s", rev1, rcsfile->path); + retval = 1; + goto error_return; + } + + if (rev2 == NULL) + { + assert (workfile != NULL); + use_file2 = workfile; + } + else + { + tmpfile2 = cvs_temp_name (); + cvs_output ("retrieving revision ", 0); + cvs_output (rev2, 0); + cvs_output ("\n", 1); + status = RCS_checkout (rcsfile, NULL, rev2, NULL, options, + tmpfile2, (RCSCHECKOUTPROC)0, NULL); + if (status > 0) + { + retval = status; + goto error_return; + } + else if (status < 0) + { + error (0, errno, + "cannot check out revision %s of %s", rev2, rcsfile->path); + return 1; + } + use_file2 = tmpfile2; + } + + RCS_output_diff_options (opts, rev1, rev2, workfile); + status = diff_execv (tmpfile1, use_file2, label1, label2, opts, RUN_TTY); + if (status >= 0) + { + retval = status; + goto error_return; + } + else if (status < 0) + { + error (0, errno, + "cannot diff %s and %s", tmpfile1, use_file2); + retval = 1; + goto error_return; + } + + error_return: + { + int save_noexec = noexec; + noexec = 0; + if (unlink_file (tmpfile1) < 0) + { + if (!existence_error (errno)) + error (0, errno, "cannot remove temp file %s", tmpfile1); + } + noexec = save_noexec; + } + free (tmpfile1); + if (tmpfile2 != NULL) + { + int save_noexec = noexec; + noexec = 0; + if (unlink_file (tmpfile2) < 0) + { + if (!existence_error (errno)) + error (0, errno, "cannot remove temp file %s", tmpfile2); + } + noexec = save_noexec; + free (tmpfile2); + } + + return retval; +} + + +/* Show differences between two files. This is the start of a diff library. + + Some issues: + + * Should option parsing be part of the library or the caller? The + former allows the library to add options without changing the callers, + but it causes various problems. One is that something like --brief really + wants special handling in CVS, and probably the caller should retain + some flexibility in this area. Another is online help (the library could + have some feature for providing help, but how does that interact with + the help provided by the caller directly?). Another is that as things + stand currently, there is no separate namespace for diff options versus + "cvs diff" options like -l (that is, if the library adds an option which + conflicts with a CVS option, it is trouble). + + * This isn't required for a first-cut diff library, but if there + would be a way for the caller to specify the timestamps that appear + in the diffs (rather than the library getting them from the files), + that would clean up the kludgy utime() calls in patch.c. + + Show differences between FILE1 and FILE2. Either one can be + DEVNULL to indicate a nonexistent file (same as an empty file + currently, I suspect, but that may be an issue in and of itself). + OPTIONS is a list of diff options, or "" if none. At a minimum, + CVS expects that -c (update.c, patch.c) and -n (update.c) will be + supported. Other options, like -u, --speed-large-files, &c, will + be specified if the user specified them. + + OUT is a filename to send the diffs to, or RUN_TTY to send them to + stdout. Error messages go to stderr. Return value is 0 for + success, -1 for a failure which set errno, 1 for success (and some + differences were found), or >1 for a failure which printed a + message on stderr. */ + +int +diff_exec (file1, file2, options, out) + char *file1; + char *file2; + char *options; + char *out; +{ + char *args; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* If either file1 or file2 are special files, pretend they are + /dev/null. Reason: suppose a file that represents a block + special device in one revision becomes a regular file. CVS + must find the `difference' between these files, but a special + file contains no data useful for calculating this metric. The + safe thing to do is to treat the special file as an empty file, + thus recording the regular file's full contents. Doing so will + create extremely large deltas at the point of transition + between device files and regular files, but this is probably + very rare anyway. + + There may be ways around this, but I think they are fraught + with danger. -twp */ + + if (preserve_perms && + strcmp (file1, DEVNULL) != 0 && + strcmp (file2, DEVNULL) != 0) + { + struct stat sb1, sb2; + + if (CVS_LSTAT (file1, &sb1) < 0) + error (1, errno, "cannot get file information for %s", file1); + if (CVS_LSTAT (file2, &sb2) < 0) + error (1, errno, "cannot get file information for %s", file2); + + if (!S_ISREG (sb1.st_mode) && !S_ISDIR (sb1.st_mode)) + file1 = DEVNULL; + if (!S_ISREG (sb2.st_mode) && !S_ISDIR (sb2.st_mode)) + file2 = DEVNULL; + } +#endif + + args = xmalloc (strlen (options) + 10); + /* The first word in this string is used only for error reporting. */ + sprintf (args, "diff %s", options); + call_diff_setup (args); + call_diff_arg (file1); + call_diff_arg (file2); + free (args); + + return call_diff (out); +} + +int +diff_execv (file1, file2, label1, label2, options, out) + char *file1; + char *file2; + char *label1; + char *label2; + char *options; + char *out; +{ + char *args; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + /* Pretend that special files are /dev/null for purposes of making + diffs. See comments in diff_exec. */ + + if (preserve_perms && + strcmp (file1, DEVNULL) != 0 && + strcmp (file2, DEVNULL) != 0) + { + struct stat sb1, sb2; + + if (CVS_LSTAT (file1, &sb1) < 0) + error (1, errno, "cannot get file information for %s", file1); + if (CVS_LSTAT (file2, &sb2) < 0) + error (1, errno, "cannot get file information for %s", file2); + + if (!S_ISREG (sb1.st_mode) && !S_ISDIR (sb1.st_mode)) + file1 = DEVNULL; + if (!S_ISREG (sb2.st_mode) && !S_ISDIR (sb2.st_mode)) + file2 = DEVNULL; + } +#endif + + args = xmalloc (strlen (options) + 10); + /* The first word in this string is used only for error reporting. */ + /* I guess we are pretty confident that options starts with a space. */ + sprintf (args, "diff%s", options); + call_diff_setup (args); + if (label1) + call_diff_arg (label1); + if (label2) + call_diff_arg (label2); + call_diff_arg (file1); + call_diff_arg (file2); + free (args); + + return call_diff (out); +} + +/* Print the options passed to DIFF, in the format used by rcsdiff. + The rcsdiff code that produces this output is extremely hairy, and + it is not clear how rcsdiff decides which options to print and + which not to print. The code below reproduces every rcsdiff run + that I have seen. */ + +static void +RCS_output_diff_options (opts, rev1, rev2, workfile) + char *opts; + char *rev1; + char *rev2; + char *workfile; +{ + char *tmp; + + tmp = (char *) xmalloc (strlen (opts) + strlen (rev1) + 10); + + sprintf (tmp, "diff%s -r%s", opts, rev1); + cvs_output (tmp, 0); + free (tmp); + + if (rev2) + { + cvs_output (" -r", 3); + cvs_output (rev2, 0); + } + else + { + assert (workfile != NULL); + cvs_output (" ", 1); + cvs_output (workfile, 0); + } + cvs_output ("\n", 1); +} diff --git a/gnu/dist/cvs/src/recurse.c b/gnu/dist/cvs/src/recurse.c new file mode 100644 index 00000000000..ca43bf995b0 --- /dev/null +++ b/gnu/dist/cvs/src/recurse.c @@ -0,0 +1,1209 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * General recursion handler + * + */ + +#include "cvs.h" +#include "savecwd.h" +#include "fileattr.h" +#include "edit.h" + +static int do_dir_proc PROTO((Node * p, void *closure)); +static int do_file_proc PROTO((Node * p, void *closure)); +static void addlist PROTO((List ** listp, char *key)); +static int unroll_files_proc PROTO((Node *p, void *closure)); +static void addfile PROTO((List **listp, char *dir, char *file)); + +static char *update_dir; +static char *repository = NULL; +static List *filelist = NULL; /* holds list of files on which to operate */ +static List *dirlist = NULL; /* holds list of directories on which to operate */ + +struct recursion_frame { + FILEPROC fileproc; + FILESDONEPROC filesdoneproc; + DIRENTPROC direntproc; + DIRLEAVEPROC dirleaveproc; + void *callerdat; + Dtype flags; + int which; + int aflag; + int readlock; + int dosrcs; +}; + +static int do_recursion PROTO ((struct recursion_frame *frame)); + +/* I am half tempted to shove a struct file_info * into the struct + recursion_frame (but then we would need to modify or create a + recursion_frame for each file), or shove a struct recursion_frame * + into the struct file_info (more tempting, although it isn't completely + clear that the struct file_info should contain info about recursion + processor internals). So instead use this struct. */ + +struct frame_and_file { + struct recursion_frame *frame; + struct file_info *finfo; +}; + +/* Similarly, we need to pass the entries list to do_dir_proc. */ + +struct frame_and_entries { + struct recursion_frame *frame; + List *entries; +}; + + +/* Start a recursive command. + + Command line arguments (ARGC, ARGV) dictate the directories and + files on which we operate. In the special case of no arguments, we + default to ".". */ +int +start_recursion (fileproc, filesdoneproc, direntproc, dirleaveproc, callerdat, + argc, argv, local, which, aflag, readlock, + update_preload, dosrcs) + FILEPROC fileproc; + FILESDONEPROC filesdoneproc; + DIRENTPROC direntproc; + DIRLEAVEPROC dirleaveproc; + void *callerdat; + + int argc; + char **argv; + int local; + + /* This specifies the kind of recursion. There are several cases: + + 1. W_LOCAL is not set but W_REPOS or W_ATTIC is. The current + directory when we are called must be the repository and + recursion proceeds according to what exists in the repository. + + 2a. W_LOCAL is set but W_REPOS and W_ATTIC are not. The + current directory when we are called must be the working + directory. Recursion proceeds according to what exists in the + working directory, never (I think) consulting any part of the + repository which does not correspond to the working directory + ("correspond" == Name_Repository). + + 2b. W_LOCAL is set and so is W_REPOS or W_ATTIC. This is the + weird one. The current directory when we are called must be + the working directory. We recurse through working directories, + but we recurse into a directory if it is exists in the working + directory *or* it exists in the repository. If a directory + does not exist in the working directory, the direntproc must + either tell us to skip it (R_SKIP_ALL), or must create it (I + think those are the only two cases). */ + int which; + + int aflag; + int readlock; + char *update_preload; + int dosrcs; +{ + int i, err = 0; +#ifdef CLIENT_SUPPORT + List *args_to_send_when_finished = NULL; +#endif + List *files_by_dir = NULL; + struct recursion_frame frame; + + frame.fileproc = fileproc; + frame.filesdoneproc = filesdoneproc; + frame.direntproc = direntproc; + frame.dirleaveproc = dirleaveproc; + frame.callerdat = callerdat; + frame.flags = local ? R_SKIP_DIRS : R_PROCESS; + frame.which = which; + frame.aflag = aflag; + frame.readlock = readlock; + frame.dosrcs = dosrcs; + + expand_wild (argc, argv, &argc, &argv); + + if (update_preload == NULL) + update_dir = xstrdup (""); + else + update_dir = xstrdup (update_preload); + + /* clean up from any previous calls to start_recursion */ + if (repository) + { + free (repository); + repository = (char *) NULL; + } + if (filelist) + dellist (&filelist); /* FIXME-krp: no longer correct. */ + if (dirlist) + dellist (&dirlist); + +#ifdef SERVER_SUPPORT + if (server_active) + { + for (i = 0; i < argc; ++i) + server_pathname_check (argv[i]); + } +#endif + + if (argc == 0) + { + int just_subdirs = (which & W_LOCAL) && !isdir (CVSADM); + +#ifdef CLIENT_SUPPORT + if (!just_subdirs + && CVSroot_cmdline == NULL + && client_active) + { + char *root = Name_Root (NULL, update_dir); + if (root && strcmp (root, current_root) != 0) + /* We're skipping this directory because it is for + a different root. Therefore, we just want to + do the subdirectories only. Processing files would + cause a working directory from one repository to be + processed against a different repository, which could + cause all kinds of spurious conflicts and such. + + Question: what about the case of "cvs update foo" + where we process foo/bar and not foo itself? That + seems to be handled somewhere (else) but why should + it be a separate case? Needs investigation... */ + just_subdirs = 1; + } +#endif + + /* + * There were no arguments, so we'll probably just recurse. The + * exception to the rule is when we are called from a directory + * without any CVS administration files. That has always meant to + * process each of the sub-directories, so we pretend like we were + * called with the list of sub-dirs of the current dir as args + */ + if (just_subdirs) + { + dirlist = Find_Directories ((char *) NULL, W_LOCAL, (List *) NULL); + /* If there are no sub-directories, there is a certain logic in + favor of doing nothing, but in fact probably the user is just + confused about what directory they are in, or whether they + cvs add'd a new directory. In the case of at least one + sub-directory, at least when we recurse into them we + notice (hopefully) whether they are under CVS control. */ + if (list_isempty (dirlist)) + { + if (update_dir[0] == '\0') + error (0, 0, "in directory .:"); + else + error (0, 0, "in directory %s:", update_dir); + error (1, 0, + "there is no version here; run '%s checkout' first", + program_name); + } +#ifdef CLIENT_SUPPORT + else if (client_active && server_started) + { + /* In the the case "cvs update foo bar baz", a call to + send_file_names in update.c will have sent the + appropriate "Argument" commands to the server. In + this case, that won't have happened, so we need to + do it here. While this example uses "update", this + generalizes to other commands. */ + + /* This is the same call to Find_Directories as above. + FIXME: perhaps it would be better to write a + function that duplicates a list. */ + args_to_send_when_finished = Find_Directories ((char *) NULL, + W_LOCAL, + (List *) NULL); + } +#endif + } + else + addlist (&dirlist, "."); + + goto do_the_work; + } + + + /* + * There were arguments, so we have to handle them by hand. To do + * that, we set up the filelist and dirlist with the arguments and + * call do_recursion. do_recursion recognizes the fact that the + * lists are non-null when it starts and doesn't update them. + * + * explicitly named directories are stored in dirlist. + * explicitly named files are stored in filelist. + * other possibility is named entities whicha are not currently in + * the working directory. + */ + + for (i = 0; i < argc; i++) + { + /* if this argument is a directory, then add it to the list of + directories. */ + + if (!wrap_name_has (argv[i], WRAP_TOCVS) && isdir (argv[i])) + addlist (&dirlist, argv[i]); + else + { + /* otherwise, split argument into directory and component names. */ + char *dir; + char *comp; + char *file_to_try; + + /* Now break out argv[i] into directory part (DIR) and file part (COMP). + DIR and COMP will each point to a newly malloc'd string. */ + dir = xstrdup (argv[i]); + comp = last_component (dir); + if (comp == dir) + { + /* no dir component. What we have is an implied "./" */ + dir = xstrdup("."); + } + else + { + char *p = comp; + + p[-1] = '\0'; + comp = xstrdup (p); + } + + /* if this argument exists as a file in the current + working directory tree, then add it to the files list. */ + + if (!(which & W_LOCAL)) + { + /* If doing rtag, we've done a chdir to the repository. */ + file_to_try = xmalloc (strlen (argv[i]) + sizeof (RCSEXT) + 5); + sprintf (file_to_try, "%s%s", argv[i], RCSEXT); + } + else + file_to_try = xstrdup (argv[i]); + + if (isfile (file_to_try)) + addfile (&files_by_dir, dir, comp); + else if (isdir (dir)) + { + if ((which & W_LOCAL) && isdir (CVSADM) +#ifdef CLIENT_SUPPORT + && !client_active +#endif + ) + { + /* otherwise, look for it in the repository. */ + char *tmp_update_dir; + char *repos; + char *reposfile; + + tmp_update_dir = xmalloc (strlen (update_dir) + + strlen (dir) + + 5); + strcpy (tmp_update_dir, update_dir); + + if (*tmp_update_dir != '\0') + (void) strcat (tmp_update_dir, "/"); + + (void) strcat (tmp_update_dir, dir); + + /* look for it in the repository. */ + repos = Name_Repository (dir, tmp_update_dir); + reposfile = xmalloc (strlen (repos) + + strlen (comp) + + 5); + (void) sprintf (reposfile, "%s/%s", repos, comp); + free (repos); + + if (!wrap_name_has (comp, WRAP_TOCVS) && isdir (reposfile)) + addlist (&dirlist, argv[i]); + else + addfile (&files_by_dir, dir, comp); + + free (tmp_update_dir); + free (reposfile); + } + else + addfile (&files_by_dir, dir, comp); + } + else + error (1, 0, "no such directory `%s'", dir); + + free (file_to_try); + free (dir); + free (comp); + } + } + + /* At this point we have looped over all named arguments and built + a coupla lists. Now we unroll the lists, setting up and + calling do_recursion. */ + + err += walklist (files_by_dir, unroll_files_proc, (void *) &frame); + dellist(&files_by_dir); + + /* then do_recursion on the dirlist. */ + if (dirlist != NULL) + { + do_the_work: + err += do_recursion (&frame); + } + + /* Free the data which expand_wild allocated. */ + free_names (&argc, argv); + + free (update_dir); + update_dir = NULL; + +#ifdef CLIENT_SUPPORT + if (args_to_send_when_finished != NULL) + { + /* FIXME (njc): in the multiroot case, we don't want to send + argument commands for those top-level directories which do + not contain any subdirectories which have files checked out + from current_root. If we do, and two repositories have a + module with the same name, nasty things could happen. + + This is hard. Perhaps we should send the Argument commands + later in this procedure, after we've had a chance to notice + which directores we're using (after do_recursion has been + called once). This means a _lot_ of rewriting, however. + + What we need to do for that to happen is descend the tree + and construct a list of directories which are checked out + from current_cvsroot. Now, we eliminate from the list all + of those directories which are immediate subdirectories of + another directory in the list. To say that the opposite + way, we keep the directories which are not immediate + subdirectories of any other in the list. Here's a picture: + + a + / \ + B C + / \ + D e + / \ + F G + / \ + H I + + The node in capitals are those directories which are + checked out from current_cvsroot. We want the list to + contain B, C, F, and G. D, H, and I are not included, + because their parents are also checked out from + current_cvsroot. + + The algorithm should be: + + 1) construct a tree of all directory names where each + element contains a directory name and a flag which notes if + that directory is checked out from current_cvsroot + + a0 + / \ + B1 C1 + / \ + D1 e0 + / \ + F1 G1 + / \ + H1 I1 + + 2) Recursively descend the tree. For each node, recurse + before processing the node. If the flag is zero, do + nothing. If the flag is 1, check the node's parent. If + the parent's flag is one, change the current entry's flag + to zero. + + a0 + / \ + B1 C1 + / \ + D0 e0 + / \ + F1 G1 + / \ + H0 I0 + + 3) Walk the tree and spit out "Argument" commands to tell + the server which directories to munge. + + Yuck. It's not clear this is worth spending time on, since + we might want to disable cvs commands entirely from + directories that do not have CVSADM files... + + Anyways, the solution as it stands has modified server.c + (dirswitch) to create admin files [via server.c + (create_adm_p)] in all path elements for a client's + "Directory xxx" command, which forces the server to descend + and serve the files there. client.c (send_file_names) has + also been modified to send only those arguments which are + appropriate to current_root. + + */ + + /* Construct a fake argc/argv pair. */ + + int our_argc = 0, i; + char **our_argv = NULL; + + if (! list_isempty (args_to_send_when_finished)) + { + Node *head, *p; + + head = args_to_send_when_finished->list; + + /* count the number of nodes */ + i = 0; + for (p = head->next; p != head; p = p->next) + i++; + our_argc = i; + + /* create the argument vector */ + our_argv = (char **) xmalloc (sizeof (char *) * our_argc); + + /* populate it */ + i = 0; + for (p = head->next; p != head; p = p->next) + our_argv[i++] = xstrdup (p->key); + } + + /* We don't want to expand widcards, since we've just created + a list of directories directly from the filesystem. */ + send_file_names (our_argc, our_argv, 0); + + /* Free our argc/argv. */ + if (our_argv != NULL) + { + for (i = 0; i < our_argc; i++) + free (our_argv[i]); + free (our_argv); + } + + dellist (&args_to_send_when_finished); + } +#endif + + return (err); +} + +/* + * Implement the recursive policies on the local directory. This may be + * called directly, or may be called by start_recursion + */ +static int +do_recursion (frame) + struct recursion_frame *frame; +{ + int err = 0; + int dodoneproc = 1; + char *srepository; + List *entries = NULL; + int should_readlock; + int process_this_directory = 1; + + /* do nothing if told */ + if (frame->flags == R_SKIP_ALL) + return (0); + + should_readlock = noexec ? 0 : frame->readlock; + + /* The fact that locks are not active here is what makes us fail to have + the + + If someone commits some changes in one cvs command, + then an update by someone else will either get all the + changes, or none of them. + + property (see node Concurrency in cvs.texinfo). + + The most straightforward fix would just to readlock the whole + tree before starting an update, but that means that if a commit + gets blocked on a big update, it might need to wait a *long* + time. + + A more adequate fix would be a two-pass design for update, + checkout, etc. The first pass would go through the repository, + with the whole tree readlocked, noting what versions of each + file we want to get. The second pass would release all locks + (except perhaps short-term locks on one file at a + time--although I think RCS already deals with this) and + actually get the files, specifying the particular versions it wants. + + This could be sped up by separating out the data needed for the + first pass into a separate file(s)--for example a file + attribute for each file whose value contains the head revision + for each branch. The structure should be designed so that + commit can relatively quickly update the information for a + single file or a handful of files (file attributes, as + implemented in Jan 96, are probably acceptable; improvements + would be possible such as branch attributes which are in + separate files for each branch). */ + +#if defined(SERVER_SUPPORT) && defined(SERVER_FLOWCONTROL) + /* + * Now would be a good time to check to see if we need to stop + * generating data, to give the buffers a chance to drain to the + * remote client. We should not have locks active at this point. + */ + if (server_active + /* If there are writelocks around, we cannot pause here. */ + && (should_readlock || noexec)) + server_pause_check(); +#endif + + /* Check the value in CVSADM_ROOT and see if it's in the list. If + not, add it to our lists of CVS/Root directories and do not + process the files in this directory. Otherwise, continue as + usual. THIS_ROOT might be NULL if we're doing an initial + checkout -- check before using it. The default should be that + we process a directory's contents and only skip those contents + if a CVS/Root file exists. + + If we're running the server, we want to process all + directories, since we're guaranteed to have only one CVSROOT -- + our own. */ + + if ( + /* If -d was specified, it should override CVS/Root. + + In the single-repository case, it is long-standing CVS behavior + and makes sense - the user might want another access method, + another server (which mounts the same repository), &c. + + In the multiple-repository case, -d overrides all CVS/Root + files. That is the only plausible generalization I can + think of. */ + CVSroot_cmdline == NULL + +#ifdef SERVER_SUPPORT + && ! server_active +#endif + ) + { + char *this_root = Name_Root ((char *) NULL, update_dir); + if (this_root != NULL) + { + if (findnode (root_directories, this_root) == NULL) + { + /* Add it to our list. */ + + Node *n = getnode (); + n->type = NT_UNKNOWN; + n->key = xstrdup (this_root); + + if (addnode (root_directories, n)) + error (1, 0, "cannot add new CVSROOT %s", this_root); + + } + + process_this_directory = (strcmp (current_root, this_root) == 0); + + free (this_root); + } + } + + /* + * Fill in repository with the current repository + */ + if (frame->which & W_LOCAL) + { + if (isdir (CVSADM)) + repository = Name_Repository ((char *) NULL, update_dir); + else + repository = NULL; + } + else + { + repository = xgetwd (); + if (repository == NULL) + error (1, errno, "could not get working directory"); + } + srepository = repository; /* remember what to free */ + + fileattr_startdir (repository); + + /* + * The filesdoneproc needs to be called for each directory where files + * processed, or each directory that is processed by a call where no + * directories were passed in. In fact, the only time we don't want to + * call back the filesdoneproc is when we are processing directories that + * were passed in on the command line (or in the special case of `.' when + * we were called with no args + */ + if (dirlist != NULL && filelist == NULL) + dodoneproc = 0; + + /* + * If filelist or dirlist is already set, we don't look again. Otherwise, + * find the files and directories + */ + if (filelist == NULL && dirlist == NULL) + { + /* both lists were NULL, so start from scratch */ + if (frame->fileproc != NULL && frame->flags != R_SKIP_FILES) + { + int lwhich = frame->which; + + /* be sure to look in the attic if we have sticky tags/date */ + if ((lwhich & W_ATTIC) == 0) + if (isreadable (CVSADM_TAG)) + lwhich |= W_ATTIC; + + /* In the !(which & W_LOCAL) case, we filled in repository + earlier in the function. In the (which & W_LOCAL) case, + the Find_Names function is going to look through the + Entries file. If we do not have a repository, that + does not make sense, so we insist upon having a + repository at this point. Name_Repository will give a + reasonable error message. */ + if (repository == NULL) + repository = Name_Repository ((char *) NULL, update_dir); + + /* find the files and fill in entries if appropriate */ + if (process_this_directory) + { + filelist = Find_Names (repository, lwhich, frame->aflag, + &entries); + if (filelist == NULL) + { + error (0, 0, "skipping directory %s", update_dir); + /* Note that Find_Directories and the filesdoneproc + in particular would do bad things ("? foo.c" in + the case of some filesdoneproc's). */ + goto skip_directory; + } + } + } + + /* find sub-directories if we will recurse */ + if (frame->flags != R_SKIP_DIRS) + dirlist = Find_Directories ( + process_this_directory ? repository : NULL, + frame->which, entries); + } + else + { + /* something was passed on the command line */ + if (filelist != NULL && frame->fileproc != NULL) + { + /* we will process files, so pre-parse entries */ + if (frame->which & W_LOCAL) + entries = Entries_Open (frame->aflag, NULL); + } + } + + /* process the files (if any) */ + if (process_this_directory && filelist != NULL && frame->fileproc) + { + struct file_info finfo_struct; + struct frame_and_file frfile; + + /* read lock it if necessary */ + if (should_readlock && repository && Reader_Lock (repository) != 0) + error (1, 0, "read lock failed - giving up"); + +#ifdef CLIENT_SUPPORT + /* For the server, we handle notifications in a completely different + place (server_notify). For local, we can't do them here--we don't + have writelocks in place, and there is no way to get writelocks + here. */ + if (client_active) + notify_check (repository, update_dir); +#endif /* CLIENT_SUPPORT */ + + finfo_struct.repository = repository; + finfo_struct.update_dir = update_dir; + finfo_struct.entries = entries; + /* do_file_proc will fill in finfo_struct.file. */ + + frfile.finfo = &finfo_struct; + frfile.frame = frame; + + /* process the files */ + err += walklist (filelist, do_file_proc, &frfile); + + /* unlock it */ + if (should_readlock) + Lock_Cleanup (); + + /* clean up */ + dellist (&filelist); + } + + /* call-back files done proc (if any) */ + if (process_this_directory && dodoneproc && frame->filesdoneproc != NULL) + err = frame->filesdoneproc (frame->callerdat, err, repository, + update_dir[0] ? update_dir : ".", + entries); + + skip_directory: + fileattr_write (); + fileattr_free (); + + /* process the directories (if necessary) */ + if (dirlist != NULL) + { + struct frame_and_entries frent; + + frent.frame = frame; + frent.entries = entries; + err += walklist (dirlist, do_dir_proc, (void *) &frent); + } +#if 0 + else if (frame->dirleaveproc != NULL) + err += frame->dirleaveproc (frame->callerdat, ".", err, "."); +#endif + dellist (&dirlist); + + if (entries) + { + Entries_Close (entries); + entries = NULL; + } + + /* free the saved copy of the pointer if necessary */ + if (srepository) + { + free (srepository); + repository = (char *) NULL; + } + + return (err); +} + +/* + * Process each of the files in the list with the callback proc + */ +static int +do_file_proc (p, closure) + Node *p; + void *closure; +{ + struct frame_and_file *frfile = (struct frame_and_file *)closure; + struct file_info *finfo = frfile->finfo; + int ret; + + finfo->file = p->key; + finfo->fullname = xmalloc (strlen (finfo->file) + + strlen (finfo->update_dir) + + 2); + finfo->fullname[0] = '\0'; + if (finfo->update_dir[0] != '\0') + { + strcat (finfo->fullname, finfo->update_dir); + strcat (finfo->fullname, "/"); + } + strcat (finfo->fullname, finfo->file); + + if (frfile->frame->dosrcs && repository) + { + finfo->rcs = RCS_parse (finfo->file, repository); + + /* OK, without W_LOCAL the error handling becomes relatively + simple. The file names came from readdir() on the + repository and so we know any ENOENT is an error + (e.g. symlink pointing to nothing). Now, the logic could + be simpler - since we got the name from readdir, we could + just be calling RCS_parsercsfile. */ + if (finfo->rcs == NULL + && !(frfile->frame->which & W_LOCAL)) + { + error (0, 0, "could not read RCS file for %s", finfo->fullname); + free (finfo->fullname); + cvs_flushout (); + return 0; + } + } + else + finfo->rcs = (RCSNode *) NULL; + ret = frfile->frame->fileproc (frfile->frame->callerdat, finfo); + + freercsnode(&finfo->rcs); + free (finfo->fullname); + + /* Allow the user to monitor progress with tail -f. Doing this once + per file should be no big deal, but we don't want the performance + hit of flushing on every line like previous versions of CVS. */ + cvs_flushout (); + + return (ret); +} + +/* + * Process each of the directories in the list (recursing as we go) + */ +static int +do_dir_proc (p, closure) + Node *p; + void *closure; +{ + struct frame_and_entries *frent = (struct frame_and_entries *) closure; + struct recursion_frame *frame = frent->frame; + struct recursion_frame xframe; + char *dir = p->key; + char *newrepos; + List *sdirlist; + char *srepository; + Dtype dir_return = R_PROCESS; + int stripped_dot = 0; + int err = 0; + struct saved_cwd cwd; + char *saved_update_dir; + int process_this_directory = 1; + + if (fncmp (dir, CVSADM) == 0) + { + /* This seems to most often happen when users (beginning users, + generally), try "cvs ci *" or something similar. On that + theory, it is possible that we should just silently skip the + CVSADM directories, but on the other hand, using a wildcard + like this isn't necessarily a practice to encourage (it operates + only on files which exist in the working directory, unlike + regular CVS recursion). */ + + /* FIXME-reentrancy: printed_cvs_msg should be in a "command + struct" or some such, so that it gets cleared for each new + command (this is possible using the remote protocol and a + custom-written client). The struct recursion_frame is not + far back enough though, some commands (commit at least) + will call start_recursion several times. An alternate solution + would be to take this whole check and move it to a new function + validate_arguments or some such that all the commands call + and which snips the offending directory from the argc,argv + vector. */ + static int printed_cvs_msg = 0; + if (!printed_cvs_msg) + { + error (0, 0, "warning: directory %s specified in argument", + dir); + error (0, 0, "\ +but CVS uses %s for its own purposes; skipping %s directory", + CVSADM, dir); + printed_cvs_msg = 1; + } + return 0; + } + + saved_update_dir = update_dir; + update_dir = xmalloc (strlen (saved_update_dir) + + strlen (dir) + + 5); + strcpy (update_dir, saved_update_dir); + + /* set up update_dir - skip dots if not at start */ + if (strcmp (dir, ".") != 0) + { + if (update_dir[0] != '\0') + { + (void) strcat (update_dir, "/"); + (void) strcat (update_dir, dir); + } + else + (void) strcpy (update_dir, dir); + + /* + * Here we need a plausible repository name for the sub-directory. We + * create one by concatenating the new directory name onto the + * previous repository name. The only case where the name should be + * used is in the case where we are creating a new sub-directory for + * update -d and in that case the generated name will be correct. + */ + if (repository == NULL) + newrepos = xstrdup (""); + else + { + newrepos = xmalloc (strlen (repository) + strlen (dir) + 5); + sprintf (newrepos, "%s/%s", repository, dir); + } + } + else + { + if (update_dir[0] == '\0') + (void) strcpy (update_dir, dir); + + if (repository == NULL) + newrepos = xstrdup (""); + else + newrepos = xstrdup (repository); + } + + /* Check to see that the CVSADM directory, if it exists, seems to be + well-formed. It can be missing files if the user hit ^C in the + middle of a previous run. We want to (a) make this a nonfatal + error, and (b) make sure we print which directory has the + problem. + + Do this before the direntproc, so that (1) the direntproc + doesn't have to guess/deduce whether we will skip the directory + (e.g. send_dirent_proc and whether to send the directory), and + (2) so that the warm fuzzy doesn't get printed if we skip the + directory. */ + if (frame->which & W_LOCAL) + { + char *cvsadmdir; + + cvsadmdir = xmalloc (strlen (dir) + + sizeof (CVSADM_REP) + + sizeof (CVSADM_ENT) + + 80); + + strcpy (cvsadmdir, dir); + strcat (cvsadmdir, "/"); + strcat (cvsadmdir, CVSADM); + if (isdir (cvsadmdir)) + { + strcpy (cvsadmdir, dir); + strcat (cvsadmdir, "/"); + strcat (cvsadmdir, CVSADM_REP); + if (!isfile (cvsadmdir)) + { + /* Some commands like update may have printed "? foo" but + if we were planning to recurse, and don't on account of + CVS/Repository, we want to say why. */ + error (0, 0, "ignoring %s (%s missing)", update_dir, + CVSADM_REP); + dir_return = R_SKIP_ALL; + } + + /* Likewise for CVS/Entries. */ + if (dir_return != R_SKIP_ALL) + { + strcpy (cvsadmdir, dir); + strcat (cvsadmdir, "/"); + strcat (cvsadmdir, CVSADM_ENT); + if (!isfile (cvsadmdir)) + { + /* Some commands like update may have printed "? foo" but + if we were planning to recurse, and don't on account of + CVS/Repository, we want to say why. */ + error (0, 0, "ignoring %s (%s missing)", update_dir, + CVSADM_ENT); + dir_return = R_SKIP_ALL; + } + } + } + free (cvsadmdir); + } + + /* Only process this directory if the root matches. This nearly + duplicates code in do_recursion. */ + + if ( + /* If -d was specified, it should override CVS/Root. + + In the single-repository case, it is long-standing CVS behavior + and makes sense - the user might want another access method, + another server (which mounts the same repository), &c. + + In the multiple-repository case, -d overrides all CVS/Root + files. That is the only plausible generalization I can + think of. */ + CVSroot_cmdline == NULL + +#ifdef SERVER_SUPPORT + && ! server_active +#endif + ) + { + char *this_root = Name_Root (dir, update_dir); + if (this_root != NULL) + { + if (findnode (root_directories, this_root) == NULL) + { + /* Add it to our list. */ + + Node *n = getnode (); + n->type = NT_UNKNOWN; + n->key = xstrdup (this_root); + + if (addnode (root_directories, n)) + error (1, 0, "cannot add new CVSROOT %s", this_root); + + } + + process_this_directory = (strcmp (current_root, this_root) == 0); + free (this_root); + } + } + + /* call-back dir entry proc (if any) */ + if (dir_return == R_SKIP_ALL) + ; + else if (frame->direntproc != NULL) + { + /* If we're doing the actual processing, call direntproc. + Otherwise, assume that we need to process this directory + and recurse. FIXME. */ + + if (process_this_directory) + dir_return = frame->direntproc (frame->callerdat, dir, newrepos, + update_dir, frent->entries); + else + dir_return = R_PROCESS; + } + else + { + /* Generic behavior. I don't see a reason to make the caller specify + a direntproc just to get this. */ + if ((frame->which & W_LOCAL) && !isdir (dir)) + dir_return = R_SKIP_ALL; + } + + free (newrepos); + + /* only process the dir if the return code was 0 */ + if (dir_return != R_SKIP_ALL) + { + /* save our current directory and static vars */ + if (save_cwd (&cwd)) + error_exit (); + sdirlist = dirlist; + srepository = repository; + dirlist = NULL; + + /* cd to the sub-directory */ + if ( CVS_CHDIR (dir) < 0) + error (1, errno, "could not chdir to %s", dir); + + /* honor the global SKIP_DIRS (a.k.a. local) */ + if (frame->flags == R_SKIP_DIRS) + dir_return = R_SKIP_DIRS; + + /* remember if the `.' will be stripped for subsequent dirs */ + if (strcmp (update_dir, ".") == 0) + { + update_dir[0] = '\0'; + stripped_dot = 1; + } + + /* make the recursive call */ + xframe = *frame; + xframe.flags = dir_return; + err += do_recursion (&xframe); + + /* put the `.' back if necessary */ + if (stripped_dot) + (void) strcpy (update_dir, "."); + + /* call-back dir leave proc (if any) */ + if (process_this_directory && frame->dirleaveproc != NULL) + err = frame->dirleaveproc (frame->callerdat, dir, err, update_dir, + frent->entries); + + /* get back to where we started and restore state vars */ + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + dirlist = sdirlist; + repository = srepository; + } + + free (update_dir); + update_dir = saved_update_dir; + + return (err); +} + +/* + * Add a node to a list allocating the list if necessary. + */ +static void +addlist (listp, key) + List **listp; + char *key; +{ + Node *p; + + if (*listp == NULL) + *listp = getlist (); + p = getnode (); + p->type = FILES; + p->key = xstrdup (key); + if (addnode (*listp, p) != 0) + freenode (p); +} + +static void +addfile (listp, dir, file) + List **listp; + char *dir; + char *file; +{ + Node *n; + + /* add this dir. */ + addlist (listp, dir); + + n = findnode (*listp, dir); + if (n == NULL) + { + error (1, 0, "can't find recently added dir node `%s' in start_recursion.", + dir); + } + + n->type = DIRS; + addlist ((List **) &n->data, file); + return; +} + +static int +unroll_files_proc (p, closure) + Node *p; + void *closure; +{ + Node *n; + struct recursion_frame *frame = (struct recursion_frame *) closure; + int err = 0; + List *save_dirlist; + char *save_update_dir = NULL; + struct saved_cwd cwd; + + /* if this dir was also an explicitly named argument, then skip + it. We'll catch it later when we do dirs. */ + n = findnode (dirlist, p->key); + if (n != NULL) + return (0); + + /* otherwise, call dorecusion for this list of files. */ + filelist = (List *) p->data; + p->data = NULL; + save_dirlist = dirlist; + dirlist = NULL; + + if (strcmp(p->key, ".") != 0) + { + if (save_cwd (&cwd)) + error_exit (); + if ( CVS_CHDIR (p->key) < 0) + error (1, errno, "could not chdir to %s", p->key); + + save_update_dir = update_dir; + update_dir = xmalloc (strlen (save_update_dir) + + strlen (p->key) + + 5); + strcpy (update_dir, save_update_dir); + + if (*update_dir != '\0') + (void) strcat (update_dir, "/"); + + (void) strcat (update_dir, p->key); + } + + err += do_recursion (frame); + + if (save_update_dir != NULL) + { + free (update_dir); + update_dir = save_update_dir; + + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + } + + dirlist = save_dirlist; + filelist = NULL; + return(err); +} diff --git a/gnu/dist/cvs/src/release.c b/gnu/dist/cvs/src/release.c new file mode 100644 index 00000000000..06d582f4e5b --- /dev/null +++ b/gnu/dist/cvs/src/release.c @@ -0,0 +1,299 @@ +/* + * Release: "cancel" a checkout in the history log. + * + * - Enter a line in the history log indicating the "release". - If asked to, + * delete the local working directory. + */ + +#include "cvs.h" +#include "savecwd.h" +#include "getline.h" + +static const char *const release_usage[] = +{ + "Usage: %s %s [-d] directories...\n", + "\t-d\tDelete the given directory.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +#ifdef SERVER_SUPPORT +static int release_server PROTO ((int argc, char **argv)); + +/* This is the server side of cvs release. */ +static int +release_server (argc, argv) + int argc; + char **argv; +{ + int i; + + /* Note that we skip argv[0]. */ + for (i = 1; i < argc; ++i) + history_write ('F', argv[i], "", argv[i], ""); + return 0; +} + +#endif /* SERVER_SUPPORT */ + +/* There are various things to improve about this implementation: + + 1. Using run_popen to run "cvs update" could be replaced by a + fairly simple start_recursion/classify_file loop--a win for + portability, performance, and cleanliness. In particular, there is + no particularly good way to find the right "cvs". + + 2. The fact that "cvs update" contacts the server slows things down; + it undermines the case for using "cvs release" rather than "rm -rf". + However, for correctly printing "? foo" and correctly handling + CVSROOTADM_IGNORE, we currently need to contact the server. (One + idea for how to fix this is to stash a copy of CVSROOTADM_IGNORE in + the working directories; see comment at base_* in entries.c for a + few thoughts on that). + + 3. Would be nice to take processing things on the client side one step + further, and making it like edit/unedit in terms of working well if + disconnected from the network, and then sending a delayed + notification. + + 4. Having separate network turnarounds for the "Notify" request + which we do as part of unedit, and for the "release" itself, is slow + and unnecessary. */ + +int +release (argc, argv) + int argc; + char **argv; +{ + FILE *fp; + int i, c; + char *repository; + char *line = NULL; + size_t line_allocated = 0; + char *update_cmd; + char *thisarg; + int arg_start_idx; + int err = 0; + short delete_flag = 0; + struct saved_cwd cwd; + +#ifdef SERVER_SUPPORT + if (server_active) + return release_server (argc, argv); +#endif + + /* Everything from here on is client or local. */ + if (argc == -1) + usage (release_usage); + optind = 0; + while ((c = getopt (argc, argv, "+Qdq")) != -1) + { + switch (c) + { + case 'Q': + case 'q': + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'd': + delete_flag++; + break; + case '?': + default: + usage (release_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* We're going to run "cvs -n -q update" and check its output; if + * the output is sufficiently unalarming, then we release with no + * questions asked. Else we prompt, then maybe release. + * (Well, actually we ask no matter what. Our notion of "sufficiently + * unalarming" doesn't take into account "? foo.c" files, so it is + * up to the user to take note of them, at least currently + * (ignore-193 in testsuite)). + */ + /* Construct the update command. */ + update_cmd = xmalloc (strlen (program_path) + + strlen (CVSroot_original) + + 20); + sprintf (update_cmd, "%s -n -q -d %s update", + program_path, CVSroot_original); + +#ifdef CLIENT_SUPPORT + /* Start the server; we'll close it after looping. */ + if (client_active) + { + start_server (); + ign_setup (); + } +#endif /* CLIENT_SUPPORT */ + + /* Remember the directory where "cvs release" was invoked because + all args are relative to this directory and we chdir around. + */ + if (save_cwd (&cwd)) + error_exit (); + + arg_start_idx = 0; + + for (i = arg_start_idx; i < argc; i++) + { + thisarg = argv[i]; + + if (isdir (thisarg)) + { + if (CVS_CHDIR (thisarg) < 0) + { + if (!really_quiet) + error (0, errno, "can't chdir to: %s", thisarg); + continue; + } + if (!isdir (CVSADM)) + { + if (!really_quiet) + error (0, 0, "no repository directory: %s", thisarg); + if (restore_cwd (&cwd, NULL)) + error_exit (); + continue; + } + } + else + { + if (!really_quiet) + error (0, 0, "no such directory: %s", thisarg); + continue; + } + + repository = Name_Repository ((char *) NULL, (char *) NULL); + + if (!really_quiet) + { + int line_length; + + /* The "release" command piggybacks on "update", which + does the real work of finding out if anything is not + up-to-date with the repository. Then "release" prompts + the user, telling her how many files have been + modified, and asking if she still wants to do the + release. */ + fp = run_popen (update_cmd, "r"); + if (fp == NULL) + error (1, 0, "cannot run command %s", update_cmd); + + c = 0; + + while ((line_length = getline (&line, &line_allocated, fp)) >= 0) + { + if (strchr ("MARCZ", *line)) + c++; + (void) fputs (line, stdout); + } + if (line_length < 0 && !feof (fp)) + error (0, errno, "cannot read from subprocess"); + + /* If the update exited with an error, then we just want to + complain and go on to the next arg. Especially, we do + not want to delete the local copy, since it's obviously + not what the user thinks it is. */ + if ((pclose (fp)) != 0) + { + error (0, 0, "unable to release `%s'", thisarg); + free (repository); + if (restore_cwd (&cwd, NULL)) + error_exit (); + continue; + } + + printf ("You have [%d] altered files in this repository.\n", + c); + printf ("Are you sure you want to release %sdirectory `%s': ", + delete_flag ? "(and delete) " : "", thisarg); + c = !yesno (); + if (c) /* "No" */ + { + (void) fprintf (stderr, "** `%s' aborted by user choice.\n", + command_name); + free (repository); + if (restore_cwd (&cwd, NULL)) + error_exit (); + continue; + } + } + + if (1 +#ifdef CLIENT_SUPPORT + && !(client_active + && (!supported_request ("noop") + || !supported_request ("Notify"))) +#endif + ) + { + /* We are chdir'ed into the directory in question. + So don't pass args to unedit. */ + int argc = 1; + char *argv[3]; + argv[0] = "dummy"; + argv[1] = NULL; + err += unedit (argc, argv); + } + +#ifdef CLIENT_SUPPORT + if (client_active) + { + send_to_server ("Argument ", 0); + send_to_server (thisarg, 0); + send_to_server ("\012", 1); + send_to_server ("release\012", 0); + } + else +#endif /* CLIENT_SUPPORT */ + { + history_write ('F', thisarg, "", thisarg, ""); /* F == Free */ + } + + free (repository); + + if (restore_cwd (&cwd, NULL)) + error_exit (); + + if (delete_flag) + { + /* FIXME? Shouldn't this just delete the CVS-controlled + files and, perhaps, the files that would normally be + ignored and leave everything else? */ + + if (unlink_file_dir (thisarg) < 0) + error (0, errno, "deletion of directory %s failed", thisarg); + } + +#ifdef CLIENT_SUPPORT + if (client_active) + err += get_server_responses (); +#endif /* CLIENT_SUPPORT */ + } + + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* Unfortunately, client.c doesn't offer a way to close + the connection without waiting for responses. The extra + network turnaround here is quite unnecessary other than + that.... */ + send_to_server ("noop\012", 0); + err += get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + free (update_cmd); + if (line != NULL) + free (line); + return err; +} diff --git a/gnu/dist/cvs/src/remove.c b/gnu/dist/cvs/src/remove.c new file mode 100644 index 00000000000..2dacdf15988 --- /dev/null +++ b/gnu/dist/cvs/src/remove.c @@ -0,0 +1,277 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Remove a File + * + * Removes entries from the present version. The entries will be removed from + * the RCS repository upon the next "commit". + * + * "remove" accepts no options, only file names that are to be removed. The + * file must not exist in the current directory for "remove" to work + * correctly. + */ + +#include "cvs.h" + +#ifdef CLIENT_SUPPORT +static int remove_force_fileproc PROTO ((void *callerdat, + struct file_info *finfo)); +#endif +static int remove_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static Dtype remove_dirproc PROTO ((void *callerdat, char *dir, + char *repos, char *update_dir, + List *entries)); + +static int force; +static int local; +static int removed_files; +static int existing_files; + +static const char *const remove_usage[] = +{ + "Usage: %s %s [-flR] [files...]\n", + "\t-f\tDelete the file before removing it.\n", + "\t-l\tProcess this directory only (not recursive).\n", + "\t-R\tProcess directories recursively.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +cvsremove (argc, argv) + int argc; + char **argv; +{ + int c, err; + + if (argc == -1) + usage (remove_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+flR")) != -1) + { + switch (c) + { + case 'f': + force = 1; + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (remove_usage); + break; + } + } + argc -= optind; + argv += optind; + + wrap_setup (); + +#ifdef CLIENT_SUPPORT + if (client_active) { + /* Call expand_wild so that the local removal of files will + work. It's ok to do it always because we have to send the + file names expanded anyway. */ + expand_wild (argc, argv, &argc, &argv); + + if (force) + { + if (!noexec) + { + start_recursion (remove_force_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, + (void *) NULL, argc, argv, local, W_LOCAL, + 0, 0, (char *) NULL, 0); + } + /* else FIXME should probably act as if the file doesn't exist + in doing the following checks. */ + } + + start_server (); + ign_setup (); + if (local) + send_arg("-l"); + /* FIXME: Can't we set SEND_NO_CONTENTS here? Needs investigation. */ + send_files (argc, argv, local, 0, 0); + send_file_names (argc, argv, 0); + send_to_server ("remove\012", 0); + return get_responses_and_close (); + } +#endif + + /* start the recursion processor */ + err = start_recursion (remove_fileproc, (FILESDONEPROC) NULL, + remove_dirproc, (DIRLEAVEPROC) NULL, NULL, + argc, argv, + local, W_LOCAL, 0, 1, (char *) NULL, 1); + + if (removed_files) + error (0, 0, "use '%s commit' to remove %s permanently", program_name, + (removed_files == 1) ? "this file" : "these files"); + + if (existing_files) + error (0, 0, + ((existing_files == 1) ? + "%d file exists; remove it first" : + "%d files exist; remove them first"), + existing_files); + + return (err); +} + +#ifdef CLIENT_SUPPORT + +/* + * This is called via start_recursion if we are running as the client + * and the -f option was used. We just physically remove the file. + */ + +/*ARGSUSED*/ +static int +remove_force_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + if (CVS_UNLINK (finfo->file) < 0 && ! existence_error (errno)) + error (0, errno, "unable to remove %s", finfo->fullname); + return 0; +} + +#endif + +/* + * remove the file, only if it has already been physically removed + */ +/* ARGSUSED */ +static int +remove_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + Vers_TS *vers; + + if (force) + { + if (!noexec) + { + if ( CVS_UNLINK (finfo->file) < 0 && ! existence_error (errno)) + { + error (0, errno, "unable to remove %s", finfo->fullname); + } + } + /* else FIXME should probably act as if the file doesn't exist + in doing the following checks. */ + } + + vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); + + if (vers->ts_user != NULL) + { + existing_files++; + if (!quiet) + error (0, 0, "file `%s' still in working directory", + finfo->fullname); + } + else if (vers->vn_user == NULL) + { + if (!quiet) + error (0, 0, "nothing known about `%s'", finfo->fullname); + } + else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0') + { + char *fname; + + /* + * It's a file that has been added, but not commited yet. So, + * remove the ,t file for it and scratch it from the + * entries file. */ + Scratch_Entry (finfo->entries, finfo->file); + fname = xmalloc (strlen (finfo->file) + + sizeof (CVSADM) + + sizeof (CVSEXT_LOG) + + 10); + (void) sprintf (fname, "%s/%s%s", CVSADM, finfo->file, CVSEXT_LOG); + if (unlink_file (fname) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", CVSEXT_LOG); + if (!quiet) + error (0, 0, "removed `%s'", finfo->fullname); + +#ifdef SERVER_SUPPORT + if (server_active) + server_checked_in (finfo->file, finfo->update_dir, finfo->repository); +#endif + free (fname); + } + else if (vers->vn_user[0] == '-') + { + if (!quiet) + error (0, 0, "file `%s' already scheduled for removal", + finfo->fullname); + } + else if (vers->tag != NULL && isdigit ((unsigned char) *vers->tag)) + { + /* Commit will just give an error, and so there seems to be + little reason to allow the remove. I mean, conflicts that + arise out of parallel development are one thing, but conflicts + that arise from sticky tags are quite another. + + I would have thought that non-branch sticky tags should be the + same but at least now, removing a file with a non-branch sticky + tag means to delete the tag from the file. I'm not sure that + is a good behavior, but until it is changed, we need to allow + it. */ + error (0, 0, "\ +cannot remove file `%s' which has a numeric sticky tag of `%s'", + finfo->fullname, vers->tag); + } + else + { + char *fname; + + /* Re-register it with a negative version number. */ + fname = xmalloc (strlen (vers->vn_user) + 5); + (void) strcpy (fname, "-"); + (void) strcat (fname, vers->vn_user); + Register (finfo->entries, finfo->file, fname, vers->ts_rcs, vers->options, + vers->tag, vers->date, vers->ts_conflict); + if (!quiet) + error (0, 0, "scheduling `%s' for removal", finfo->fullname); + removed_files++; + +#ifdef SERVER_SUPPORT + if (server_active) + server_checked_in (finfo->file, finfo->update_dir, finfo->repository); +#endif + free (fname); + } + + freevers_ts (&vers); + return (0); +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +remove_dirproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + char *dir; + char *repos; + char *update_dir; + List *entries; +{ + if (!quiet) + error (0, 0, "Removing %s", update_dir); + return (R_PROCESS); +} diff --git a/gnu/dist/cvs/src/repos.c b/gnu/dist/cvs/src/repos.c new file mode 100644 index 00000000000..2738665758a --- /dev/null +++ b/gnu/dist/cvs/src/repos.c @@ -0,0 +1,206 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + */ + +#include <assert.h> +#include "cvs.h" +#include "getline.h" + +/* Determine the name of the RCS repository for directory DIR in the + current working directory, or for the current working directory + itself if DIR is NULL. Returns the name in a newly-malloc'd + string. On error, gives a fatal error and does not return. + UPDATE_DIR is the path from where cvs was invoked (for use in error + messages), and should contain DIR as its last component. + UPDATE_DIR can be NULL to signify the directory in which cvs was + invoked. */ + +char * +Name_Repository (dir, update_dir) + char *dir; + char *update_dir; +{ + FILE *fpin; + char *xupdate_dir; + char *repos = NULL; + size_t repos_allocated = 0; + char *tmp; + char *cp; + + if (update_dir && *update_dir) + xupdate_dir = update_dir; + else + xupdate_dir = "."; + + if (dir != NULL) + { + tmp = xmalloc (strlen (dir) + sizeof (CVSADM_REP) + 10); + (void) sprintf (tmp, "%s/%s", dir, CVSADM_REP); + } + else + tmp = xstrdup (CVSADM_REP); + + /* + * The assumption here is that the repository is always contained in the + * first line of the "Repository" file. + */ + fpin = CVS_FOPEN (tmp, "r"); + + if (fpin == NULL) + { + int save_errno = errno; + char *cvsadm; + + if (dir != NULL) + { + cvsadm = xmalloc (strlen (dir) + sizeof (CVSADM) + 10); + (void) sprintf (cvsadm, "%s/%s", dir, CVSADM); + } + else + cvsadm = xstrdup (CVSADM); + + if (!isdir (cvsadm)) + { + error (0, 0, "in directory %s:", xupdate_dir); + error (1, 0, "there is no version here; do '%s checkout' first", + program_name); + } + free (cvsadm); + + if (existence_error (save_errno)) + { + /* FIXME: This is a very poorly worded error message. It + occurs at least in the case where the user manually + creates a directory named CVS, so the error message + should be more along the lines of "CVS directory found + without administrative files; use CVS to create the CVS + directory, or rename it to something else if the + intention is to store something besides CVS + administrative files". */ + error (0, 0, "in directory %s:", xupdate_dir); + error (1, 0, "*PANIC* administration files missing"); + } + + error (1, save_errno, "cannot open %s", tmp); + } + + if (getline (&repos, &repos_allocated, fpin) < 0) + { + /* FIXME: should be checking for end of file separately. */ + error (0, 0, "in directory %s:", xupdate_dir); + error (1, errno, "cannot read %s", CVSADM_REP); + } + if (fclose (fpin) < 0) + error (0, errno, "cannot close %s", tmp); + free (tmp); + + if ((cp = strrchr (repos, '\n')) != NULL) + *cp = '\0'; /* strip the newline */ + + /* + * If this is a relative repository pathname, turn it into an absolute + * one by tacking on the CVSROOT environment variable. If the CVSROOT + * environment variable is not set, die now. + */ + if (strcmp (repos, "..") == 0 || strncmp (repos, "../", 3) == 0) + { + error (0, 0, "in directory %s:", xupdate_dir); + error (0, 0, "`..'-relative repositories are not supported."); + error (1, 0, "illegal source repository"); + } + if (! isabsolute(repos)) + { + char *newrepos; + + if (CVSroot_original == NULL) + { + error (0, 0, "in directory %s:", xupdate_dir); + error (0, 0, "must set the CVSROOT environment variable\n"); + error (0, 0, "or specify the '-d' option to %s.", program_name); + error (1, 0, "illegal repository setting"); + } + newrepos = xmalloc (strlen (CVSroot_directory) + strlen (repos) + 10); + (void) sprintf (newrepos, "%s/%s", CVSroot_directory, repos); + free (repos); + repos = newrepos; + } + + Sanitize_Repository_Name (repos); + + return repos; +} + +/* + * Return a pointer to the repository name relative to CVSROOT from a + * possibly fully qualified repository + */ +char * +Short_Repository (repository) + char *repository; +{ + if (repository == NULL) + return (NULL); + + /* If repository matches CVSroot at the beginning, strip off CVSroot */ + /* And skip leading '/' in rep, in case CVSroot ended with '/'. */ + if (strncmp (CVSroot_directory, repository, + strlen (CVSroot_directory)) == 0) + { + char *rep = repository + strlen (CVSroot_directory); + return (*rep == '/') ? rep+1 : rep; + } + else + return (repository); +} + +/* Sanitize the repository name (in place) by removing trailing + * slashes and a trailing "." if present. It should be safe for + * callers to use strcat and friends to create repository names. + * Without this check, names like "/path/to/repos/./foo" and + * "/path/to/repos//foo" would be created. For example, one + * significant case is the CVSROOT-detection code in commit.c. It + * decides whether or not it needs to rebuild the administrative file + * database by doing a string compare. If we've done a `cvs co .' to + * get the CVSROOT files, "/path/to/repos/./CVSROOT" and + * "/path/to/repos/CVSROOT" are the arguments that are compared! + * + * This function ends up being called from the same places as + * strip_path, though what it does is much more conservative. Many + * comments about this operation (which was scattered around in + * several places in the source code) ran thus: + * + * ``repository ends with "/."; omit it. This sort of thing used + * to be taken care of by strip_path. Now we try to be more + * selective. I suspect that it would be even better to push it + * back further someday, so that the trailing "/." doesn't get into + * repository in the first place, but we haven't taken things that + * far yet.'' --Jim Kingdon (recurse.c, 07-Sep-97) + * + * Ahh, all too true. The major consideration is RELATIVE_REPOS. If + * the "/." doesn't end up in the repository while RELATIVE_REPOS is + * defined, there will be nothing in the CVS/Repository file. I + * haven't verified that the remote protocol will handle that + * correctly yet, so I've not made that change. */ + +void +Sanitize_Repository_Name (repository) + char *repository; +{ + size_t len; + + assert (repository != NULL); + + strip_trailing_slashes (repository); + + len = strlen (repository); + if (len >= 2 + && repository[len - 1] == '.' + && ISDIRSEP (repository[len - 2])) + { + repository[len - 2] = '\0'; + } +} diff --git a/gnu/dist/cvs/src/root.c b/gnu/dist/cvs/src/root.c new file mode 100644 index 00000000000..d88c6f4b55a --- /dev/null +++ b/gnu/dist/cvs/src/root.c @@ -0,0 +1,574 @@ +/* + * Copyright (c) 1992, Mark D. Baushke + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Name of Root + * + * Determine the path to the CVSROOT and set "Root" accordingly. + */ + +#include "cvs.h" +#include "getline.h" + +/* Printable names for things in the CVSroot_method enum variable. + Watch out if the enum is changed in cvs.h! */ + +char *method_names[] = { + "local", "server (rsh)", "pserver", "kserver", "gserver", "ext", "fork" +}; + +#ifndef DEBUG + +char * +Name_Root (dir, update_dir) + char *dir; + char *update_dir; +{ + FILE *fpin; + char *ret, *xupdate_dir; + char *root = NULL; + size_t root_allocated = 0; + char *tmp; + char *cvsadm; + char *cp; + + if (update_dir && *update_dir) + xupdate_dir = update_dir; + else + xupdate_dir = "."; + + if (dir != NULL) + { + cvsadm = xmalloc (strlen (dir) + sizeof (CVSADM) + 10); + (void) sprintf (cvsadm, "%s/%s", dir, CVSADM); + tmp = xmalloc (strlen (dir) + sizeof (CVSADM_ROOT) + 10); + (void) sprintf (tmp, "%s/%s", dir, CVSADM_ROOT); + } + else + { + cvsadm = xstrdup (CVSADM); + tmp = xstrdup (CVSADM_ROOT); + } + + /* + * Do not bother looking for a readable file if there is no cvsadm + * directory present. + * + * It is possible that not all repositories will have a CVS/Root + * file. This is ok, but the user will need to specify -d + * /path/name or have the environment variable CVSROOT set in + * order to continue. */ + if ((!isdir (cvsadm)) || (!isreadable (tmp))) + { + ret = NULL; + goto out; + } + + /* + * The assumption here is that the CVS Root is always contained in the + * first line of the "Root" file. + */ + fpin = open_file (tmp, "r"); + + if (getline (&root, &root_allocated, fpin) < 0) + { + /* FIXME: should be checking for end of file separately; errno + is not set in that case. */ + error (0, 0, "in directory %s:", xupdate_dir); + error (0, errno, "cannot read %s", CVSADM_ROOT); + error (0, 0, "please correct this problem"); + ret = NULL; + goto out; + } + (void) fclose (fpin); + if ((cp = strrchr (root, '\n')) != NULL) + *cp = '\0'; /* strip the newline */ + + /* + * root now contains a candidate for CVSroot. It must be an + * absolute pathname or specify a remote server. + */ + + if ( +#ifdef CLIENT_SUPPORT + (strchr (root, ':') == NULL) && +#endif + ! isabsolute (root)) + { + error (0, 0, "in directory %s:", xupdate_dir); + error (0, 0, + "ignoring %s because it does not contain an absolute pathname.", + CVSADM_ROOT); + ret = NULL; + goto out; + } + +#ifdef CLIENT_SUPPORT + if ((strchr (root, ':') == NULL) && !isdir (root)) +#else /* ! CLIENT_SUPPORT */ + if (!isdir (root)) +#endif /* CLIENT_SUPPORT */ + { + error (0, 0, "in directory %s:", xupdate_dir); + error (0, 0, + "ignoring %s because it specifies a non-existent repository %s", + CVSADM_ROOT, root); + ret = NULL; + goto out; + } + + /* allocate space to return and fill it in */ + strip_trailing_slashes (root); + ret = xstrdup (root); + out: + free (cvsadm); + free (tmp); + if (root != NULL) + free (root); + return (ret); +} + +/* + * Write the CVS/Root file so that the environment variable CVSROOT + * and/or the -d option to cvs will be validated or not necessary for + * future work. + */ +void +Create_Root (dir, rootdir) + char *dir; + char *rootdir; +{ + FILE *fout; + char *tmp; + + if (noexec) + return; + + /* record the current cvs root */ + + if (rootdir != NULL) + { + if (dir != NULL) + { + tmp = xmalloc (strlen (dir) + sizeof (CVSADM_ROOT) + 10); + (void) sprintf (tmp, "%s/%s", dir, CVSADM_ROOT); + } + else + tmp = xstrdup (CVSADM_ROOT); + + fout = open_file (tmp, "w+"); + if (fprintf (fout, "%s\n", rootdir) < 0) + error (1, errno, "write to %s failed", tmp); + if (fclose (fout) == EOF) + error (1, errno, "cannot close %s", tmp); + free (tmp); + } +} + +#endif /* ! DEBUG */ + + +/* The root_allow_* stuff maintains a list of legal CVSROOT + directories. Then we can check against them when a remote user + hands us a CVSROOT directory. */ + +static unsigned int root_allow_count; +static char **root_allow_vector; +static unsigned int root_allow_size; + +void +root_allow_add (arg) + char *arg; +{ + char *p; + + if (root_allow_size <= root_allow_count) + { + if (root_allow_size == 0) + { + root_allow_size = 1; + root_allow_vector = + (char **) malloc (root_allow_size * sizeof (char *)); + } + else + { + root_allow_size *= 2; + root_allow_vector = + (char **) realloc (root_allow_vector, + root_allow_size * sizeof (char *)); + } + + if (root_allow_vector == NULL) + { + no_memory: + /* Strictly speaking, we're not supposed to output anything + now. But we're about to exit(), give it a try. */ + printf ("E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket + subsystems on NT and OS2 or dealing with windows + and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (EXIT_FAILURE); + } + } + p = malloc (strlen (arg) + 1); + if (p == NULL) + goto no_memory; + strcpy (p, arg); + root_allow_vector[root_allow_count++] = p; +} + +void +root_allow_free () +{ + if (root_allow_vector != NULL) + free (root_allow_vector); + root_allow_count = 0; + root_allow_size = 0; +} + +int +root_allow_ok (arg) + char *arg; +{ + unsigned int i; + + if (root_allow_count == 0) + { + /* Probably someone upgraded from CVS before 1.9.10 to 1.9.10 + or later without reading the documentation about + --allow-root. Printing an error here doesn't disclose any + particularly useful information to an attacker because a + CVS server configured in this way won't let *anyone* in. */ + + /* Note that we are called from a context where we can spit + back "error" rather than waiting for the next request which + expects responses. */ + printf ("\ +error 0 Server configuration missing --allow-root in inetd.conf\n"); + error_exit (); + } + + for (i = 0; i < root_allow_count; ++i) + if (strcmp (root_allow_vector[i], arg) == 0) + return 1; + return 0; +} + +/* This global variable holds the global -d option. It is NULL if -d + was not used, which means that we must get the CVSroot information + from the CVSROOT environment variable or from a CVS/Root file. */ + +char *CVSroot_cmdline; + +/* Parse a CVSROOT variable into its constituent parts -- method, + * username, hostname, directory. The prototypical CVSROOT variable + * looks like: + * + * :method:user@host:path + * + * Some methods may omit fields; local, for example, doesn't need user + * and host. + * + * Returns zero on success, non-zero on failure. */ + +char *CVSroot_original = NULL; /* the CVSroot that was passed in */ +int client_active; /* nonzero if we are doing remote access */ +CVSmethod CVSroot_method; /* one of the enum values defined in cvs.h */ +char *CVSroot_username; /* the username or NULL if method == local */ +char *CVSroot_hostname; /* the hostname or NULL if method == local */ +char *CVSroot_directory; /* the directory name */ + +int +parse_cvsroot (CVSroot) + char *CVSroot; +{ + static int cvsroot_parsed = 0; + char *cvsroot_copy, *p; + int check_hostname; + + /* Don't go through the trouble twice. */ + if (cvsroot_parsed) + { + error (0, 0, "WARNING (parse_cvsroot): someone called me twice!\n"); + return 0; + } + + CVSroot_original = xstrdup (CVSroot); + cvsroot_copy = xstrdup (CVSroot); + + if ((*cvsroot_copy == ':')) + { + char *method = ++cvsroot_copy; + + /* Access method specified, as in + * "cvs -d :pserver:user@host:/path", + * "cvs -d :local:e:\path", + * "cvs -d :kserver:user@host:/path", or + * "cvs -d :fork:/path". + * We need to get past that part of CVSroot before parsing the + * rest of it. + */ + + if (! (p = strchr (method, ':'))) + { + error (0, 0, "bad CVSroot: %s", CVSroot); + return 1; + } + *p = '\0'; + cvsroot_copy = ++p; + + /* Now we have an access method -- see if it's valid. */ + + if (strcmp (method, "local") == 0) + CVSroot_method = local_method; + else if (strcmp (method, "pserver") == 0) + CVSroot_method = pserver_method; + else if (strcmp (method, "kserver") == 0) + CVSroot_method = kserver_method; + else if (strcmp (method, "gserver") == 0) + CVSroot_method = gserver_method; + else if (strcmp (method, "server") == 0) + CVSroot_method = server_method; + else if (strcmp (method, "ext") == 0) + CVSroot_method = ext_method; + else if (strcmp (method, "fork") == 0) + CVSroot_method = fork_method; + else + { + error (0, 0, "unknown method in CVSroot: %s", CVSroot); + return 1; + } + } + else + { + /* If the method isn't specified, assume + SERVER_METHOD/EXT_METHOD if the string contains a colon or + LOCAL_METHOD otherwise. */ + + CVSroot_method = ((strchr (cvsroot_copy, ':')) +#ifdef RSH_NOT_TRANSPARENT + ? server_method +#else + ? ext_method +#endif + : local_method); + } + + client_active = (CVSroot_method != local_method); + + /* Check for username/hostname if we're not LOCAL_METHOD. */ + + CVSroot_username = NULL; + CVSroot_hostname = NULL; + + if ((CVSroot_method != local_method) + && (CVSroot_method != fork_method)) + { + /* Check to see if there is a username in the string. */ + + if ((p = strchr (cvsroot_copy, '@'))) + { + CVSroot_username = cvsroot_copy; + *p = '\0'; + cvsroot_copy = ++p; + if (*CVSroot_username == '\0') + CVSroot_username = NULL; + } + + if ((p = strchr (cvsroot_copy, ':'))) + { + CVSroot_hostname = cvsroot_copy; + *p = '\0'; + cvsroot_copy = ++p; + + if (*CVSroot_hostname == '\0') + CVSroot_hostname = NULL; + } + } + + CVSroot_directory = cvsroot_copy; + +#if ! defined (CLIENT_SUPPORT) && ! defined (DEBUG) + if (CVSroot_method != local_method) + { + error (0, 0, "Your CVSROOT is set for a remote access method"); + error (0, 0, "but your CVS executable doesn't support it"); + error (0, 0, "(%s)", CVSroot); + return 1; + } +#endif + + /* Do various sanity checks. */ + + if (CVSroot_username && ! CVSroot_hostname) + { + error (0, 0, "missing hostname in CVSROOT: %s", CVSroot); + return 1; + } + + check_hostname = 0; + switch (CVSroot_method) + { + case local_method: + case fork_method: + if (CVSroot_username || CVSroot_hostname) + { + error (0, 0, "can't specify hostname and username in CVSROOT"); + error (0, 0, "when using %s access method", + CVSroot_method == local_method ? "local" : "fork"); + error (0, 0, "(%s)", CVSroot); + return 1; + } + /* cvs.texinfo has always told people that CVSROOT must be an + absolute pathname. Furthermore, attempts to use a relative + pathname produced various errors (I couldn't get it to work), + so there would seem to be little risk in making this a fatal + error. */ + if (!isabsolute (CVSroot_directory)) + error (1, 0, "CVSROOT %s must be an absolute pathname", + CVSroot_directory); + break; + case kserver_method: +#ifndef HAVE_KERBEROS + error (0, 0, "Your CVSROOT is set for a kerberos access method"); + error (0, 0, "but your CVS executable doesn't support it"); + error (0, 0, "(%s)", CVSroot); + return 1; +#else + check_hostname = 1; + break; +#endif + case gserver_method: +#ifndef HAVE_GSSAPI + error (0, 0, "Your CVSROOT is set for a GSSAPI access method"); + error (0, 0, "but your CVS executable doesn't support it"); + error (0, 0, "(%s)", CVSroot); + return 1; +#else + check_hostname = 1; + break; +#endif + case server_method: + case ext_method: + case pserver_method: + check_hostname = 1; + break; + } + + if (check_hostname) + { + if (! CVSroot_hostname) + { + error (0, 0, "didn't specify hostname in CVSROOT: %s", CVSroot); + return 1; + } + } + + if (*CVSroot_directory == '\0') + { + error (0, 0, "missing directory in CVSROOT: %s", CVSroot); + return 1; + } + + /* Hooray! We finally parsed it! */ + return 0; +} + + +/* Set up the global CVSroot* variables as if we're using the local + repository DIR. DIR must point to storage which will last for the + rest of the CVS invocation (for example, the caller might malloc it + and never free it, or free it just before exiting CVS). */ + +void +set_local_cvsroot (dir) + char *dir; +{ + CVSroot_original = dir; + CVSroot_method = local_method; + CVSroot_directory = CVSroot_original; + CVSroot_username = NULL; + CVSroot_hostname = NULL; + client_active = 0; +} + + +#ifdef DEBUG +/* This is for testing the parsing function. Use + + gcc -I. -I.. -I../lib -DDEBUG root.c -o root + + to compile. */ + +#include <stdio.h> + +char *CVSroot; +char *program_name = "testing"; +char *command_name = "parse_cvsroot"; /* XXX is this used??? */ + +/* Toy versions of various functions when debugging under unix. Yes, + these make various bad assumptions, but they're pretty easy to + debug when something goes wrong. */ + +void +error_exit PROTO ((void)) +{ + exit (1); +} + +char * +xstrdup (str) + const char *str; +{ + return strdup (str); +} + +int +isabsolute (dir) + const char *dir; +{ + return (dir && (*dir == '/')); +} + +void +main (argc, argv) + int argc; + char *argv[]; +{ + program_name = argv[0]; + + if (argc != 2) + { + fprintf (stderr, "Usage: %s <CVSROOT>\n", program_name); + exit (2); + } + + if (parse_cvsroot (argv[1])) + { + fprintf (stderr, "%s: Parsing failed.\n", program_name); + exit (1); + } + printf ("CVSroot: %s\n", argv[1]); + printf ("CVSroot_method: %s\n", method_names[CVSroot_method]); + printf ("CVSroot_username: %s\n", + CVSroot_username ? CVSroot_username : "NULL"); + printf ("CVSroot_hostname: %s\n", + CVSroot_hostname ? CVSroot_hostname : "NULL"); + printf ("CVSroot_directory: %s\n", CVSroot_directory); + + exit (0); + /* NOTREACHED */ +} +#endif diff --git a/gnu/dist/cvs/src/rtag.c b/gnu/dist/cvs/src/rtag.c new file mode 100644 index 00000000000..5fd825f471d --- /dev/null +++ b/gnu/dist/cvs/src/rtag.c @@ -0,0 +1,766 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Rtag + * + * Add or delete a symbolic name to an RCS file, or a collection of RCS files. + * Uses the modules database, if necessary. + */ + +#include "cvs.h" + +static int check_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int check_filesdoneproc PROTO ((void *callerdat, int err, + char *repos, char *update_dir, + List *entries)); +static int pretag_proc PROTO((char *repository, char *filter)); +static void masterlist_delproc PROTO((Node *p)); +static void tag_delproc PROTO((Node *p)); +static int pretag_list_proc PROTO((Node *p, void *closure)); + +static Dtype rtag_dirproc PROTO ((void *callerdat, char *dir, + char *repos, char *update_dir, + List *entries)); +static int rtag_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int rtag_filesdoneproc PROTO ((void *callerdat, int err, + char *repos, char *update_dir, + List *entries)); +static int rtag_proc PROTO((int *pargc, char **argv, char *xwhere, + char *mwhere, char *mfile, int shorten, + int local_specified, char *mname, char *msg)); +static int rtag_delete PROTO((RCSNode *rcsfile)); + + +struct tag_info +{ + Ctype status; + char *rev; + char *tag; + char *options; +}; + +struct master_lists +{ + List *tlist; +}; + +static List *mtlist; +static List *tlist; + +static char *symtag; +static char *numtag; +static int numtag_validated = 0; +static int delete_flag; /* adding a tag by default */ +static int attic_too; /* remove tag from Attic files */ +static int branch_mode; /* make an automagic "branch" tag */ +static char *date; +static int local; /* recursive by default */ +static int force_tag_match = 1; /* force by default */ +static int force_tag_move; /* don't move existing tags by default */ + +static const char *const rtag_usage[] = +{ + "Usage: %s %s [-aflRnF] [-b] [-d] [-r rev|-D date] tag modules...\n", + "\t-a\tClear tag from removed files that would not otherwise be tagged.\n", + "\t-f\tForce a head revision match if tag/date not found.\n", + "\t-l\tLocal directory only, not recursive\n", + "\t-R\tProcess directories recursively.\n", + "\t-n\tNo execution of 'tag program'\n", + "\t-d\tDelete the given Tag.\n", + "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n", + "\t-r rev\tExisting revision/tag.\n", + "\t-D\tExisting date.\n", + "\t-F\tMove tag if it already exists\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +rtag (argc, argv) + int argc; + char **argv; +{ + register int i; + int c; + DBM *db; + int run_module_prog = 1; + int err = 0; + + if (argc == -1) + usage (rtag_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+FanfQqlRdbr:D:")) != -1) + { + switch (c) + { + case 'a': + attic_too = 1; + break; + case 'n': + run_module_prog = 0; + break; + case 'Q': + case 'q': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'd': + delete_flag = 1; + break; + case 'f': + force_tag_match = 0; + break; + case 'b': + branch_mode = 1; + break; + case 'r': + numtag = optarg; + break; + case 'D': + if (date) + free (date); + date = Make_Date (optarg); + break; + case 'F': + force_tag_move = 1; + break; + case '?': + default: + usage (rtag_usage); + break; + } + } + argc -= optind; + argv += optind; + if (argc < 2) + usage (rtag_usage); + symtag = argv[0]; + argc--; + argv++; + + if (date && numtag) + error (1, 0, "-r and -D options are mutually exclusive"); + if (delete_flag && branch_mode) + error (0, 0, "warning: -b ignored with -d options"); + RCS_check_tag (symtag); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* We're the client side. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (!force_tag_match) + send_arg ("-f"); + if (local) + send_arg("-l"); + if (delete_flag) + send_arg("-d"); + if (branch_mode) + send_arg("-b"); + if (force_tag_move) + send_arg("-F"); + if (!run_module_prog) + send_arg("-n"); + if (attic_too) + send_arg("-a"); + + if (numtag) + option_with_arg ("-r", numtag); + if (date) + client_senddate (date); + + send_arg (symtag); + + { + int i; + for (i = 0; i < argc; ++i) + send_arg (argv[i]); + } + + send_to_server ("rtag\012", 0); + return get_responses_and_close (); + } +#endif + + db = open_module (); + for (i = 0; i < argc; i++) + { + /* XXX last arg should be repository, but doesn't make sense here */ + history_write ('T', (delete_flag ? "D" : (numtag ? numtag : + (date ? date : "A"))), symtag, argv[i], ""); + err += do_module (db, argv[i], TAG, + delete_flag ? "Untagging" : "Tagging", + rtag_proc, (char *) NULL, 0, 0, run_module_prog, + symtag); + } + close_module (db); + return (err); +} + +/* + * callback proc for doing the real work of tagging + */ +/* ARGSUSED */ +static int +rtag_proc (pargc, argv, xwhere, mwhere, mfile, shorten, local_specified, + mname, msg) + int *pargc; + char **argv; + char *xwhere; + char *mwhere; + char *mfile; + int shorten; + int local_specified; + char *mname; + char *msg; +{ + /* Begin section which is identical to patch_proc--should this + be abstracted out somehow? */ + int err = 0; + int which; + char *repository; + char *where; + + repository = xmalloc (strlen (CVSroot_directory) + strlen (argv[0]) + + (mfile == NULL ? 0 : strlen (mfile)) + 30); + (void) sprintf (repository, "%s/%s", CVSroot_directory, argv[0]); + where = xmalloc (strlen (argv[0]) + (mfile == NULL ? 0 : strlen (mfile)) + + 10); + (void) strcpy (where, argv[0]); + + /* if mfile isn't null, we need to set up to do only part of the module */ + if (mfile != NULL) + { + char *cp; + char *path; + + /* if the portion of the module is a path, put the dir part on repos */ + if ((cp = strrchr (mfile, '/')) != NULL) + { + *cp = '\0'; + (void) strcat (repository, "/"); + (void) strcat (repository, mfile); + (void) strcat (where, "/"); + (void) strcat (where, mfile); + mfile = cp + 1; + } + + /* take care of the rest */ + path = xmalloc (strlen (repository) + strlen (mfile) + 5); + (void) sprintf (path, "%s/%s", repository, mfile); + if (isdir (path)) + { + /* directory means repository gets the dir tacked on */ + (void) strcpy (repository, path); + (void) strcat (where, "/"); + (void) strcat (where, mfile); + } + else + { + int i; + + /* a file means muck argv */ + for (i = 1; i < *pargc; i++) + free (argv[i]); + argv[1] = xstrdup (mfile); + (*pargc) = 2; + } + free (path); + } + + /* cd to the starting repository */ + if ( CVS_CHDIR (repository) < 0) + { + error (0, errno, "cannot chdir to %s", repository); + free (repository); + return (1); + } + free (repository); + /* End section which is identical to patch_proc. */ + + if (delete_flag || attic_too || (force_tag_match && numtag)) + which = W_REPOS | W_ATTIC; + else + which = W_REPOS; + + if (numtag != NULL && !numtag_validated) + { + tag_check_valid (numtag, *pargc - 1, argv + 1, local, 0, NULL); + numtag_validated = 1; + } + + /* check to make sure they are authorized to tag all the + specified files in the repository */ + + mtlist = getlist(); + err = start_recursion (check_fileproc, check_filesdoneproc, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + *pargc - 1, argv + 1, local, which, 0, 1, + where, 1); + + if (err) + { + error (1, 0, "correct the above errors first!"); + } + + /* start the recursion processor */ + err = start_recursion (rtag_fileproc, rtag_filesdoneproc, rtag_dirproc, + (DIRLEAVEPROC) NULL, NULL, + *pargc - 1, argv + 1, local, + which, 0, 0, where, 1); + free (where); + dellist(&mtlist); + + return (err); +} + +/* check file that is to be tagged */ +/* All we do here is add it to our list */ + +static int +check_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *xdir; + Node *p; + Vers_TS *vers; + + if (finfo->update_dir[0] == '\0') + xdir = "."; + else + xdir = finfo->update_dir; + if ((p = findnode (mtlist, xdir)) != NULL) + { + tlist = ((struct master_lists *) p->data)->tlist; + } + else + { + struct master_lists *ml; + + tlist = getlist (); + p = getnode (); + p->key = xstrdup (xdir); + p->type = UPDATE; + ml = (struct master_lists *) + xmalloc (sizeof (struct master_lists)); + ml->tlist = tlist; + p->data = (char *) ml; + p->delproc = masterlist_delproc; + (void) addnode (mtlist, p); + } + /* do tlist */ + p = getnode (); + p->key = xstrdup (finfo->file); + p->type = UPDATE; + p->delproc = tag_delproc; + vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); + p->data = RCS_getversion(vers->srcfile, numtag, date, force_tag_match, + (int *) NULL); + if (p->data != NULL) + { + int addit = 1; + char *oversion; + + oversion = RCS_getversion (vers->srcfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (oversion == NULL) + { + if (delete_flag) + { + /* Deleting a tag which did not exist is a noop and + should not be logged. */ + addit = 0; + } + } + else if (delete_flag) + { + free (p->data); + p->data = xstrdup (oversion); + } + else if (strcmp(oversion, p->data) == 0) + { + addit = 0; + } + else if (!force_tag_move) + { + addit = 0; + } + if (oversion != NULL) + { + free(oversion); + } + if (!addit) + { + free(p->data); + p->data = NULL; + } + } + freevers_ts (&vers); + (void) addnode (tlist, p); + return (0); +} + +static int +check_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + char *repos; + char *update_dir; + List *entries; +{ + int n; + Node *p; + + p = findnode(mtlist, update_dir); + if (p != NULL) + { + tlist = ((struct master_lists *) p->data)->tlist; + } + else + { + tlist = (List *) NULL; + } + if ((tlist == NULL) || (tlist->list->next == tlist->list)) + { + return (err); + } + if ((n = Parse_Info(CVSROOTADM_TAGINFO, repos, pretag_proc, 1)) > 0) + { + error (0, 0, "Pre-tag check failed"); + err += n; + } + return (err); +} + +static int +pretag_proc(repository, filter) + char *repository; + char *filter; +{ + if (filter[0] == '/') + { + char *s, *cp; + + s = xstrdup(filter); + for (cp=s; *cp; cp++) + { + if (isspace ((unsigned char) *cp)) + { + *cp = '\0'; + break; + } + } + if (!isfile(s)) + { + error (0, errno, "cannot find pre-tag filter '%s'", s); + free(s); + return (1); + } + free(s); + } + run_setup (filter); + run_arg (symtag); + run_arg (delete_flag ? "del" : force_tag_move ? "mov" : "add"); + run_arg (repository); + walklist(tlist, pretag_list_proc, NULL); + return (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL)); +} + +static void +masterlist_delproc(p) + Node *p; +{ + struct master_lists *ml; + + ml = (struct master_lists *)p->data; + dellist(&ml->tlist); + free(ml); + return; +} + +static void +tag_delproc(p) + Node *p; +{ + if (p->data != NULL) + { + free(p->data); + p->data = NULL; + } + return; +} + +static int +pretag_list_proc(p, closure) + Node *p; + void *closure; +{ + if (p->data != NULL) + { + run_arg(p->key); + run_arg(p->data); + } + return (0); +} + +/* + * Called to tag a particular file, as appropriate with the options that were + * set above. + */ +/* ARGSUSED */ +static int +rtag_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + RCSNode *rcsfile; + char *version, *rev; + int retcode = 0; + + /* Lock the directory if it is not already locked. We might be + able to rely on rtag_dirproc for this. */ + + /* It would be nice to provide consistency with respect to + commits; however CVS lacks the infrastructure to do that (see + Concurrency in cvs.texinfo and comment in do_recursion). We + can and will prevent simultaneous tag operations from + interfering with each other, by write locking each directory as + we enter it, and unlocking it as we leave it. */ + + lock_dir_for_write (finfo->repository); + + /* find the parsed RCS data */ + if ((rcsfile = finfo->rcs) == NULL) + return (1); + + /* + * For tagging an RCS file which is a symbolic link, you'd best be + * running with RCS 5.6, since it knows how to handle symbolic links + * correctly without breaking your link! + */ + + if (delete_flag) + return (rtag_delete (rcsfile)); + + /* + * If we get here, we are adding a tag. But, if -a was specified, we + * need to check to see if a -r or -D option was specified. If neither + * was specified and the file is in the Attic, remove the tag. + */ + if (attic_too && (!numtag && !date)) + { + if ((rcsfile->flags & VALID) && (rcsfile->flags & INATTIC)) + return (rtag_delete (rcsfile)); + } + + version = RCS_getversion (rcsfile, numtag, date, force_tag_match, + (int *) NULL); + if (version == NULL) + { + /* If -a specified, clean up any old tags */ + if (attic_too) + (void) rtag_delete (rcsfile); + + if (!quiet && !force_tag_match) + { + error (0, 0, "cannot find tag `%s' in `%s'", + numtag ? numtag : "head", rcsfile->path); + return (1); + } + return (0); + } + if (numtag + && isdigit ((unsigned char) *numtag) + && strcmp (numtag, version) != 0) + { + + /* + * We didn't find a match for the numeric tag that was specified, but + * that's OK. just pass the numeric tag on to rcs, to be tagged as + * specified. Could get here if one tried to tag "1.1.1" and there + * was a 1.1.1 branch with some head revision. In this case, we want + * the tag to reference "1.1.1" and not the revision at the head of + * the branch. Use a symbolic tag for that. + */ + rev = branch_mode ? RCS_magicrev (rcsfile, version) : numtag; + retcode = RCS_settag(rcsfile, symtag, numtag); + if (retcode == 0) + RCS_rewrite (rcsfile, NULL, NULL); + } + else + { + char *oversion; + + /* + * As an enhancement for the case where a tag is being re-applied to + * a large body of a module, make one extra call to RCS_getversion to + * see if the tag is already set in the RCS file. If so, check to + * see if it needs to be moved. If not, do nothing. This will + * likely save a lot of time when simply moving the tag to the + * "current" head revisions of a module -- which I have found to be a + * typical tagging operation. + */ + rev = branch_mode ? RCS_magicrev (rcsfile, version) : version; + oversion = RCS_getversion (rcsfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (oversion != NULL) + { + int isbranch = RCS_nodeisbranch (finfo->rcs, symtag); + + /* + * if versions the same and neither old or new are branches don't + * have to do anything + */ + if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch) + { + free (oversion); + free (version); + return (0); + } + + if (!force_tag_move) + { + /* we're NOT going to move the tag */ + (void) printf ("W %s", finfo->fullname); + + (void) printf (" : %s already exists on %s %s", + symtag, isbranch ? "branch" : "version", + oversion); + (void) printf (" : NOT MOVING tag to %s %s\n", + branch_mode ? "branch" : "version", rev); + free (oversion); + free (version); + return (0); + } + free (oversion); + } + retcode = RCS_settag(rcsfile, symtag, rev); + if (retcode == 0) + RCS_rewrite (rcsfile, NULL, NULL); + } + + if (retcode != 0) + { + error (1, retcode == -1 ? errno : 0, + "failed to set tag `%s' to revision `%s' in `%s'", + symtag, rev, rcsfile->path); + if (branch_mode) + free (rev); + free (version); + return (1); + } + if (branch_mode) + free (rev); + free (version); + return (0); +} + +/* + * If -d is specified, "force_tag_match" is set, so that this call to + * RCS_getversion() will return a NULL version string if the symbolic + * tag does not exist in the RCS file. + * + * If the -r flag was used, numtag is set, and we only delete the + * symtag from files that have numtag. + * + * This is done here because it's MUCH faster than just blindly calling + * "rcs" to remove the tag... trust me. + */ +static int +rtag_delete (rcsfile) + RCSNode *rcsfile; +{ + char *version; + int retcode; + + if (numtag) + { + version = RCS_getversion (rcsfile, numtag, (char *) NULL, 1, + (int *) NULL); + if (version == NULL) + return (0); + free (version); + } + + version = RCS_getversion (rcsfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (version == NULL) + return (0); + free (version); + + if ((retcode = RCS_deltag(rcsfile, symtag)) != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to remove tag `%s' from `%s'", symtag, + rcsfile->path); + return (1); + } + RCS_rewrite (rcsfile, NULL, NULL); + return (0); +} + +/* Clear any lock we may hold on the current directory. */ + +static int +rtag_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + char *repos; + char *update_dir; + List *entries; +{ + Lock_Cleanup (); + + return (err); +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +rtag_dirproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + char *dir; + char *repos; + char *update_dir; + List *entries; +{ + if (ignore_directory (update_dir)) + { + /* print the warm fuzzy message */ + if (!quiet) + error (0, 0, "Ignoring %s", update_dir); + return R_SKIP_ALL; + } + + if (!quiet) + error (0, 0, "%s %s", delete_flag ? "Untagging" : "Tagging", + update_dir); + return (R_PROCESS); +} + + + diff --git a/gnu/dist/cvs/src/run.c b/gnu/dist/cvs/src/run.c new file mode 100644 index 00000000000..41f445756d6 --- /dev/null +++ b/gnu/dist/cvs/src/run.c @@ -0,0 +1,439 @@ +/* run.c --- routines for executing subprocesses. + + This file is part of GNU CVS. + + GNU CVS 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, 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. */ + +#include "cvs.h" + +#ifndef HAVE_UNISTD_H +extern int execvp PROTO((char *file, char **argv)); +#endif + +static void run_add_arg PROTO((const char *s)); + +extern char *strtok (); + +/* + * To exec a program under CVS, first call run_setup() to setup initial + * arguments. The argument to run_setup will be parsed into whitespace + * separated words and added to the global run_argv list. + * + * Then, optionally call run_arg() for each additional argument that you'd like + * to pass to the executed program. + * + * Finally, call run_exec() to execute the program with the specified arguments. + * The execvp() syscall will be used, so that the PATH is searched correctly. + * File redirections can be performed in the call to run_exec(). + */ +static char **run_argv; +static int run_argc; +static int run_argc_allocated; + +/* VARARGS */ +void +run_setup (prog) + const char *prog; +{ + char *cp; + int i; + char *run_prog; + + /* clean out any malloc'ed values from run_argv */ + for (i = 0; i < run_argc; i++) + { + if (run_argv[i]) + { + free (run_argv[i]); + run_argv[i] = (char *) 0; + } + } + run_argc = 0; + + run_prog = xstrdup (prog); + + /* put each word into run_argv, allocating it as we go */ + for (cp = strtok (run_prog, " \t"); cp; cp = strtok ((char *) NULL, " \t")) + run_add_arg (cp); + free (run_prog); +} + +void +run_arg (s) + const char *s; +{ + run_add_arg (s); +} + +static void +run_add_arg (s) + const char *s; +{ + /* allocate more argv entries if we've run out */ + if (run_argc >= run_argc_allocated) + { + run_argc_allocated += 50; + run_argv = (char **) xrealloc ((char *) run_argv, + run_argc_allocated * sizeof (char **)); + } + + if (s) + run_argv[run_argc++] = xstrdup (s); + else + run_argv[run_argc] = (char *) 0; /* not post-incremented on purpose! */ +} + +int +run_exec (stin, stout, sterr, flags) + const char *stin; + const char *stout; + const char *sterr; + int flags; +{ + int shin, shout, sherr; + int mode_out, mode_err; + int status; + int rc = -1; + int rerrno = 0; + int pid, w; + +#ifdef POSIX_SIGNALS + sigset_t sigset_mask, sigset_omask; + struct sigaction act, iact, qact; + +#else +#ifdef BSD_SIGNALS + int mask; + struct sigvec vec, ivec, qvec; + +#else + RETSIGTYPE (*istat) (), (*qstat) (); +#endif +#endif + + if (trace) + { +#ifdef SERVER_SUPPORT + cvs_outerr (server_active ? "S" : " ", 1); +#endif + cvs_outerr ("-> system(", 0); + run_print (stderr); + cvs_outerr (")\n", 0); + } + if (noexec && (flags & RUN_REALLY) == 0) + return (0); + + /* make sure that we are null terminated, since we didn't calloc */ + run_add_arg ((char *) 0); + + /* setup default file descriptor numbers */ + shin = 0; + shout = 1; + sherr = 2; + + /* set the file modes for stdout and stderr */ + mode_out = mode_err = O_WRONLY | O_CREAT; + mode_out |= ((flags & RUN_STDOUT_APPEND) ? O_APPEND : O_TRUNC); + mode_err |= ((flags & RUN_STDERR_APPEND) ? O_APPEND : O_TRUNC); + + if (stin && (shin = open (stin, O_RDONLY)) == -1) + { + rerrno = errno; + error (0, errno, "cannot open %s for reading (prog %s)", + stin, run_argv[0]); + goto out0; + } + if (stout && (shout = open (stout, mode_out, 0666)) == -1) + { + rerrno = errno; + error (0, errno, "cannot open %s for writing (prog %s)", + stout, run_argv[0]); + goto out1; + } + if (sterr && (flags & RUN_COMBINED) == 0) + { + if ((sherr = open (sterr, mode_err, 0666)) == -1) + { + rerrno = errno; + error (0, errno, "cannot open %s for writing (prog %s)", + sterr, run_argv[0]); + goto out2; + } + } + + /* Make sure we don't flush this twice, once in the subprocess. */ + fflush (stdout); + fflush (stderr); + + /* The output files, if any, are now created. Do the fork and dups. + + We use vfork not so much for a performance boost (the + performance boost, if any, is modest on most modern unices), + but for the sake of systems without a memory management unit, + which find it difficult or impossible to implement fork at all + (e.g. Amiga). The other solution is spawn (see + windows-NT/run.c). */ + +#ifdef HAVE_VFORK + pid = vfork (); +#else + pid = fork (); +#endif + if (pid == 0) + { + if (shin != 0) + { + (void) dup2 (shin, 0); + (void) close (shin); + } + if (shout != 1) + { + (void) dup2 (shout, 1); + (void) close (shout); + } + if (flags & RUN_COMBINED) + (void) dup2 (1, 2); + else if (sherr != 2) + { + (void) dup2 (sherr, 2); + (void) close (sherr); + } + + /* dup'ing is done. try to run it now */ + (void) execvp (run_argv[0], run_argv); + error (0, errno, "cannot exec %s", run_argv[0]); + _exit (127); + } + else if (pid == -1) + { + rerrno = errno; + goto out; + } + + /* the parent. Ignore some signals for now */ +#ifdef POSIX_SIGNALS + if (flags & RUN_SIGIGNORE) + { + act.sa_handler = SIG_IGN; + (void) sigemptyset (&act.sa_mask); + act.sa_flags = 0; + (void) sigaction (SIGINT, &act, &iact); + (void) sigaction (SIGQUIT, &act, &qact); + } + else + { + (void) sigemptyset (&sigset_mask); + (void) sigaddset (&sigset_mask, SIGINT); + (void) sigaddset (&sigset_mask, SIGQUIT); + (void) sigprocmask (SIG_SETMASK, &sigset_mask, &sigset_omask); + } +#else +#ifdef BSD_SIGNALS + if (flags & RUN_SIGIGNORE) + { + memset ((char *) &vec, 0, sizeof (vec)); + vec.sv_handler = SIG_IGN; + (void) sigvec (SIGINT, &vec, &ivec); + (void) sigvec (SIGQUIT, &vec, &qvec); + } + else + mask = sigblock (sigmask (SIGINT) | sigmask (SIGQUIT)); +#else + istat = signal (SIGINT, SIG_IGN); + qstat = signal (SIGQUIT, SIG_IGN); +#endif +#endif + + /* wait for our process to die and munge return status */ +#ifdef POSIX_SIGNALS + while ((w = waitpid (pid, &status, 0)) == -1 && errno == EINTR) + ; +#else + while ((w = wait (&status)) != pid) + { + if (w == -1 && errno != EINTR) + break; + } +#endif + + if (w == -1) + { + rc = -1; + rerrno = errno; + } +#ifndef VMS /* status is return status */ + else if (WIFEXITED (status)) + rc = WEXITSTATUS (status); + else if (WIFSIGNALED (status)) + { + if (WTERMSIG (status) == SIGPIPE) + error (1, 0, "broken pipe"); + rc = 2; + } + else + rc = 1; +#else /* VMS */ + rc = WEXITSTATUS (status); +#endif /* VMS */ + + /* restore the signals */ +#ifdef POSIX_SIGNALS + if (flags & RUN_SIGIGNORE) + { + (void) sigaction (SIGINT, &iact, (struct sigaction *) NULL); + (void) sigaction (SIGQUIT, &qact, (struct sigaction *) NULL); + } + else + (void) sigprocmask (SIG_SETMASK, &sigset_omask, (sigset_t *) NULL); +#else +#ifdef BSD_SIGNALS + if (flags & RUN_SIGIGNORE) + { + (void) sigvec (SIGINT, &ivec, (struct sigvec *) NULL); + (void) sigvec (SIGQUIT, &qvec, (struct sigvec *) NULL); + } + else + (void) sigsetmask (mask); +#else + (void) signal (SIGINT, istat); + (void) signal (SIGQUIT, qstat); +#endif +#endif + + /* cleanup the open file descriptors */ + out: + if (sterr) + (void) close (sherr); + out2: + if (stout) + (void) close (shout); + out1: + if (stin) + (void) close (shin); + + out0: + if (rerrno) + errno = rerrno; + return (rc); +} + +void +run_print (fp) + FILE *fp; +{ + int i; + void (*outfn) PROTO ((const char *, size_t)); + + if (fp == stderr) + outfn = cvs_outerr; + else if (fp == stdout) + outfn = cvs_output; + else + { + error (1, 0, "internal error: bad argument to run_print"); + /* Solely to placate gcc -Wall. + FIXME: it'd be better to use a function named `fatal' that + is known never to return. Then kludges wouldn't be necessary. */ + outfn = NULL; + } + + for (i = 0; i < run_argc; i++) + { + (*outfn) ("'", 1); + (*outfn) (run_argv[i], 0); + (*outfn) ("'", 1); + if (i != run_argc - 1) + (*outfn) (" ", 1); + } +} + +/* Return value is NULL for error, or if noexec was set. If there was an + error, return NULL and I'm not sure whether errno was set (the Red Hat + Linux 4.1 popen manpage was kind of vague but discouraging; and the noexec + case complicates this even aside from popen behavior). */ + +FILE * +run_popen (cmd, mode) + const char *cmd; + const char *mode; +{ + if (trace) + (void) fprintf (stderr, "%s-> run_popen(%s,%s)\n", + CLIENT_SERVER_STR, cmd, mode); + if (noexec) + return (NULL); + + return (popen (cmd, mode)); +} + +int +piped_child (command, tofdp, fromfdp) + char **command; + int *tofdp; + int *fromfdp; +{ + int pid; + int to_child_pipe[2]; + int from_child_pipe[2]; + + if (pipe (to_child_pipe) < 0) + error (1, errno, "cannot create pipe"); + if (pipe (from_child_pipe) < 0) + error (1, errno, "cannot create pipe"); + +#ifdef USE_SETMODE_BINARY + setmode (to_child_pipe[0], O_BINARY); + setmode (to_child_pipe[1], O_BINARY); + setmode (from_child_pipe[0], O_BINARY); + setmode (from_child_pipe[1], O_BINARY); +#endif + +#ifdef HAVE_VFORK + pid = vfork (); +#else + pid = fork (); +#endif + if (pid < 0) + error (1, errno, "cannot fork"); + if (pid == 0) + { + if (dup2 (to_child_pipe[0], STDIN_FILENO) < 0) + error (1, errno, "cannot dup2 pipe"); + if (close (to_child_pipe[1]) < 0) + error (1, errno, "cannot close pipe"); + if (close (from_child_pipe[0]) < 0) + error (1, errno, "cannot close pipe"); + if (dup2 (from_child_pipe[1], STDOUT_FILENO) < 0) + error (1, errno, "cannot dup2 pipe"); + + execvp (command[0], command); + error (1, errno, "cannot exec %s", command[0]); + } + if (close (to_child_pipe[0]) < 0) + error (1, errno, "cannot close pipe"); + if (close (from_child_pipe[1]) < 0) + error (1, errno, "cannot close pipe"); + + *tofdp = to_child_pipe[1]; + *fromfdp = from_child_pipe[0]; + return pid; +} + + +void +close_on_exec (fd) + int fd; +{ +#if defined (FD_CLOEXEC) && defined (F_SETFD) + if (fcntl (fd, F_SETFD, 1)) + error (1, errno, "can't set close-on-exec flag on %d", fd); +#endif +} diff --git a/gnu/dist/cvs/src/scramble.c b/gnu/dist/cvs/src/scramble.c new file mode 100644 index 00000000000..77376faa82b --- /dev/null +++ b/gnu/dist/cvs/src/scramble.c @@ -0,0 +1,245 @@ +/* + * Trivially encode strings to protect them from innocent eyes (i.e., + * inadvertent password compromises, like a network administrator + * who's watching packets for legitimate reasons and accidentally sees + * the password protocol go by). + * + * This is NOT secure encryption. + * + * It would be tempting to encode the password according to username + * and repository, so that the same password would encode to a + * different string when used with different usernames and/or + * repositories. However, then users would not be able to cut and + * paste passwords around. They're not supposed to anyway, but we all + * know they will, and there's no reason to make it harder for them if + * we're not trying to provide real security anyway. + */ + +/* Set this to test as a standalone program. */ +/* #define DIAGNOSTIC */ + +#ifndef DIAGNOSTIC +#include "cvs.h" +#else /* ! DIAGNOSTIC */ +/* cvs.h won't define this for us */ +#define AUTH_CLIENT_SUPPORT +#define xmalloc malloc +/* Use "gcc -fwritable-strings". */ +#include <stdio.h> +#include <stdio.h> +#include <string.h> +#endif /* ! DIAGNOSTIC */ + +#if defined(AUTH_CLIENT_SUPPORT) || defined(AUTH_SERVER_SUPPORT) + +/* Map characters to each other randomly and symmetrically, A <--> B. + * + * We divide the ASCII character set into 3 domains: control chars (0 + * thru 31), printing chars (32 through 126), and "meta"-chars (127 + * through 255). The control chars map _to_ themselves, the printing + * chars map _among_ themselves, and the meta chars map _among_ + * themselves. Why is this thus? + * + * No character in any of these domains maps to a character in another + * domain, because I'm not sure what characters are legal in + * passwords, or what tools people are likely to use to cut and paste + * them. It seems prudent not to introduce control or meta chars, + * unless the user introduced them first. And having the control + * chars all map to themselves insures that newline and + * carriage-return are safely handled. + */ + +static unsigned char +shifts[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87, + 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105, + 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35, + 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56, + 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48, + 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223, + 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190, + 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193, + 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212, + 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246, + 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176, + 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127, + 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195, + 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152 }; + + +/* SCRAMBLE and DESCRAMBLE work like this: + * + * scramble(STR) returns SCRM, a scrambled copy of STR. SCRM[0] is a + * single letter indicating the scrambling method. As of this + * writing, the only legal method is 'A', but check the code for more + * up-to-date information. The copy will have been allocated with + * malloc(). + * + * descramble(SCRM) returns STR, again in its own malloc'd space. + * descramble() uses SCRM[0] to determine which method of unscrambling + * to use. If it does not recognize the method, it dies with error. + */ + +/* Return a malloc'd, scrambled version of STR. */ +char * +scramble (str) + char *str; +{ + int i; + char *s; + + /* +2 to hold the 'A' prefix that indicates which version of + scrambling this is (the first, obviously, since we only do one + kind of scrambling so far), and then the '\0' of course. */ + s = (char *) xmalloc (strlen (str) + 2); + + /* Scramble (TM) version prefix. */ + s[0] = 'A'; + strcpy (s + 1, str); + + for (i = 1; s[i]; i++) + s[i] = shifts[(unsigned char)(s[i])]; + + return s; +} + +/* Decode the string in place. */ +char * +descramble (str) + char *str; +{ + char *s; + int i; + + /* For now we can only handle one kind of scrambling. In the future + there may be other kinds, and this `if' will become a `switch'. */ + if (str[0] != 'A') +#ifndef DIAGNOSTIC + error (1, 0, "descramble: unknown scrambling method"); +#else /* DIAGNOSTIC */ + { + fprintf (stderr, "descramble: unknown scrambling method\n", str); + fflush (stderr); + exit (EXIT_FAILURE); + } +#endif /* DIAGNOSTIC */ + + /* Method `A' is symmetrical, so scramble again to decrypt. */ + s = scramble (str + 1); + + /* Shift the whole string one char to the left, pushing the unwanted + 'A' off the left end. Safe, because s is null-terminated. */ + for (i = 0; s[i]; i++) + s[i] = s[i + 1]; + + return s; +} + +#endif /* (AUTH_CLIENT_SUPPORT || AUTH_SERVER_SUPPORT) from top of file */ + +#ifdef DIAGNOSTIC +int +main () +{ + int i; + char *e, *m, biggie[256]; + + char *cleartexts[5]; + cleartexts[0] = "first"; + cleartexts[1] = "the second"; + cleartexts[2] = "this is the third"; + cleartexts[3] = "$#% !!\\3"; + cleartexts[4] = biggie; + + /* Set up the most important test string: */ + /* Can't have a real ASCII zero in the string, because we want to + use printf, so we substitute the character zero. */ + biggie[0] = '0'; + /* The rest of the string gets straight ascending ASCII. */ + for (i = 1; i < 256; i++) + biggie[i] = i; + + /* Test all the strings. */ + for (i = 0; i < 5; i++) + { + printf ("clear%d: %s\n", i, cleartexts[i]); + e = scramble (cleartexts[i]); + printf ("scram%d: %s\n", i, e); + m = descramble (e); + free (e); + printf ("clear%d: %s\n\n", i, m); + free (m); + } + + fflush (stdout); + return 0; +} +#endif /* DIAGNOSTIC */ + +/* + * ;;; The Emacs Lisp that did the dirty work ;;; + * (progn + * + * ;; Helper func. + * (defun random-elt (lst) + * (let* ((len (length lst)) + * (rnd (random len))) + * (nth rnd lst))) + * + * ;; A list of all characters under 127, each appearing once. + * (setq non-meta-chars + * (let ((i 0) + * (l nil)) + * (while (< i 127) + * (setq l (cons i l) + * i (1+ i))) + * l)) + * + * ;; A list of all characters 127 and above, each appearing once. + * (setq meta-chars + * (let ((i 127) + * (l nil)) + * (while (< i 256) + * (setq l (cons i l) + * i (1+ i))) + * l)) + * + * ;; A vector that will hold the chars in a random order. + * (setq scrambled-chars (make-vector 256 0)) + * + * ;; These characters should map to themselves. + * (let ((i 0)) + * (while (< i 32) + * (aset scrambled-chars i i) + * (setq non-meta-chars (delete i non-meta-chars) + * i (1+ i)))) + * + * ;; Assign random (but unique) values, within the non-meta chars. + * (let ((i 32)) + * (while (< i 127) + * (let ((ch (random-elt non-meta-chars))) + * (if (= 0 (aref scrambled-chars i)) + * (progn + * (aset scrambled-chars i ch) + * (aset scrambled-chars ch i) + * (setq non-meta-chars (delete ch non-meta-chars) + * non-meta-chars (delete i non-meta-chars)))) + * (setq i (1+ i))))) + * + * ;; Assign random (but unique) values, within the non-meta chars. + * (let ((i 127)) + * (while (< i 256) + * (let ((ch (random-elt meta-chars))) + * (if (= 0 (aref scrambled-chars i)) + * (progn + * (aset scrambled-chars i ch) + * (aset scrambled-chars ch i) + * (setq meta-chars (delete ch meta-chars) + * meta-chars (delete i meta-chars)))) + * (setq i (1+ i))))) + * + * ;; Now use the `scrambled-chars' vector to get your C array. + * ) + */ diff --git a/gnu/dist/cvs/src/server.c b/gnu/dist/cvs/src/server.c new file mode 100644 index 00000000000..1d1fe11b92f --- /dev/null +++ b/gnu/dist/cvs/src/server.c @@ -0,0 +1,6376 @@ +/* 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, 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. */ + +#include <assert.h> +#include "cvs.h" +#include "watch.h" +#include "edit.h" +#include "fileattr.h" +#include "getline.h" +#include "buffer.h" + +#ifdef SERVER_SUPPORT + +#ifdef HAVE_WINSOCK_H +#include <winsock.h> +#endif + +#if defined (AUTH_SERVER_SUPPORT) || defined (HAVE_KERBEROS) || defined (HAVE_GSSAPI) +#include <sys/socket.h> +#endif + +#ifdef HAVE_KERBEROS +# include <netinet/in.h> +# include <krb.h> +# ifndef HAVE_KRB_GET_ERR_TEXT +# define krb_get_err_text(status) krb_err_txt[status] +# endif + +/* Information we need if we are going to use Kerberos encryption. */ +static C_Block kblock; +static Key_schedule sched; + +#endif + +#ifdef HAVE_GSSAPI + +#include <netdb.h> + +#ifdef HAVE_GSSAPI_H +#include <gssapi.h> +#endif +#ifdef HAVE_GSSAPI_GSSAPI_H +#include <gssapi/gssapi.h> +#endif +#ifdef HAVE_GSSAPI_GSSAPI_GENERIC_H +#include <gssapi/gssapi_generic.h> +#endif + +#ifndef HAVE_GSS_C_NT_HOSTBASED_SERVICE +#define GSS_C_NT_HOSTBASED_SERVICE gss_nt_service_name +#endif + +/* We use Kerberos 5 routines to map the GSSAPI credential to a user + name. */ +#include <krb5.h> + +/* We need this to wrap data. */ +static gss_ctx_id_t gcontext; + +static void gserver_authenticate_connection PROTO((void)); + +/* Whether we are already wrapping GSSAPI communication. */ +static int cvs_gssapi_wrapping; + +# ifdef ENCRYPTION +/* Whether to encrypt GSSAPI communication. We use a global variable + like this because we use the same buffer type (gssapi_wrap) to + handle both authentication and encryption, and we don't want + multiple instances of that buffer in the communication stream. */ +int cvs_gssapi_encrypt; +# endif + +#endif + +/* for select */ +#include <sys/types.h> +#ifdef HAVE_SYS_BSDTYPES_H +#include <sys/bsdtypes.h> +#endif + +#if TIME_WITH_SYS_TIME +# include <sys/time.h> +# include <time.h> +#else +# if HAVE_SYS_TIME_H +# include <sys/time.h> +# else +# include <time.h> +# endif +#endif + +#if HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif + +#ifndef O_NONBLOCK +#define O_NONBLOCK O_NDELAY +#endif + +/* EWOULDBLOCK is not defined by POSIX, but some BSD systems will + return it, rather than EAGAIN, for nonblocking writes. */ +#ifdef EWOULDBLOCK +#define blocking_error(err) ((err) == EWOULDBLOCK || (err) == EAGAIN) +#else +#define blocking_error(err) ((err) == EAGAIN) +#endif + +#ifdef AUTH_SERVER_SUPPORT +#ifdef HAVE_GETSPNAM +#include <shadow.h> +#endif +#endif /* AUTH_SERVER_SUPPORT */ + +/* For initgroups(). */ +#if HAVE_INITGROUPS +#include <grp.h> +#endif /* HAVE_INITGROUPS */ + +#ifdef AUTH_SERVER_SUPPORT + +/* The cvs username sent by the client, which might or might not be + the same as the system username the server eventually switches to + run as. CVS_Username gets set iff password authentication is + successful. */ +char *CVS_Username = NULL; + +/* Used to check that same repos is transmitted in pserver auth and in + later CVS protocol. Exported because root.c also uses. */ +static char *Pserver_Repos = NULL; + +/* Should we check for system usernames/passwords? Can be changed by + CVSROOT/config. */ +int system_auth = 1; + +#endif /* AUTH_SERVER_SUPPORT */ + + +/* While processing requests, this buffer accumulates data to be sent to + the client, and then once we are in do_cvs_command, we use it + for all the data to be sent. */ +static struct buffer *buf_to_net; + +/* This buffer is used to read input from the client. */ +static struct buffer *buf_from_net; + +/* + * This is where we stash stuff we are going to use. Format string + * which expects a single directory within it, starting with a slash. + */ +static char *server_temp_dir; + +/* This is the original value of server_temp_dir, before any possible + changes inserted by serve_max_dotdot. */ +static char *orig_server_temp_dir; + +/* Nonzero if we should keep the temp directory around after we exit. */ +static int dont_delete_temp; + +static void server_write_entries PROTO((void)); + +/* All server communication goes through buffer structures. Most of + the buffers are built on top of a file descriptor. This structure + is used as the closure field in a buffer. */ + +struct fd_buffer +{ + /* The file descriptor. */ + int fd; + /* Nonzero if the file descriptor is in blocking mode. */ + int blocking; +}; + +static struct buffer *fd_buffer_initialize + PROTO ((int, int, void (*) (struct buffer *))); +static int fd_buffer_input PROTO((void *, char *, int, int, int *)); +static int fd_buffer_output PROTO((void *, const char *, int, int *)); +static int fd_buffer_flush PROTO((void *)); +static int fd_buffer_block PROTO((void *, int)); + +/* Initialize a buffer built on a file descriptor. FD is the file + descriptor. INPUT is nonzero if this is for input, zero if this is + for output. MEMORY is the function to call when a memory error + occurs. */ + +static struct buffer * +fd_buffer_initialize (fd, input, memory) + int fd; + int input; + void (*memory) PROTO((struct buffer *)); +{ + struct fd_buffer *n; + + n = (struct fd_buffer *) xmalloc (sizeof *n); + n->fd = fd; + n->blocking = 1; + return buf_initialize (input ? fd_buffer_input : NULL, + input ? NULL : fd_buffer_output, + input ? NULL : fd_buffer_flush, + fd_buffer_block, + (int (*) PROTO((void *))) NULL, + memory, + n); +} + +/* The buffer input function for a buffer built on a file descriptor. */ + +static int +fd_buffer_input (closure, data, need, size, got) + void *closure; + char *data; + int need; + int size; + int *got; +{ + struct fd_buffer *fd = (struct fd_buffer *) closure; + int nbytes; + + if (! fd->blocking) + nbytes = read (fd->fd, data, size); + else + { + /* This case is not efficient. Fortunately, I don't think it + ever actually happens. */ + nbytes = read (fd->fd, data, need == 0 ? 1 : need); + } + + if (nbytes > 0) + { + *got = nbytes; + return 0; + } + + *got = 0; + + if (nbytes == 0) + { + /* End of file. This assumes that we are using POSIX or BSD + style nonblocking I/O. On System V we will get a zero + return if there is no data, even when not at EOF. */ + return -1; + } + + /* Some error occurred. */ + + if (blocking_error (errno)) + { + /* Everything's fine, we just didn't get any data. */ + return 0; + } + + return errno; +} + +/* The buffer output function for a buffer built on a file descriptor. */ + +static int +fd_buffer_output (closure, data, have, wrote) + void *closure; + const char *data; + int have; + int *wrote; +{ + struct fd_buffer *fd = (struct fd_buffer *) closure; + + *wrote = 0; + + while (have > 0) + { + int nbytes; + + nbytes = write (fd->fd, data, have); + + if (nbytes <= 0) + { + if (! fd->blocking + && (nbytes == 0 || blocking_error (errno))) + { + /* A nonblocking write failed to write any data. Just + return. */ + return 0; + } + + /* Some sort of error occurred. */ + + if (nbytes == 0) + return EIO; + + return errno; + } + + *wrote += nbytes; + data += nbytes; + have -= nbytes; + } + + return 0; +} + +/* The buffer flush function for a buffer built on a file descriptor. */ + +/*ARGSUSED*/ +static int +fd_buffer_flush (closure) + void *closure; +{ + /* Nothing to do. File descriptors are always flushed. */ + return 0; +} + +/* The buffer block function for a buffer built on a file descriptor. */ + +static int +fd_buffer_block (closure, block) + void *closure; + int block; +{ + struct fd_buffer *fd = (struct fd_buffer *) closure; + int flags; + + flags = fcntl (fd->fd, F_GETFL, 0); + if (flags < 0) + return errno; + + if (block) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + if (fcntl (fd->fd, F_SETFL, flags) < 0) + return errno; + + fd->blocking = block; + + return 0; +} + +/* Populate all of the directories between BASE_DIR and its relative + subdirectory DIR with CVSADM directories. Return 0 for success or + errno value. */ +static int create_adm_p PROTO((char *, char *)); + +static int +create_adm_p (base_dir, dir) + char *base_dir; + char *dir; +{ + char *dir_where_cvsadm_lives, *dir_to_register, *p, *tmp; + int retval, done; + FILE *f; + + if (strcmp (dir, ".") == 0) + return 0; /* nothing to do */ + + /* Allocate some space for our directory-munging string. */ + p = malloc (strlen (dir) + 1); + if (p == NULL) + return ENOMEM; + + dir_where_cvsadm_lives = malloc (strlen (base_dir) + strlen (dir) + 100); + if (dir_where_cvsadm_lives == NULL) + return ENOMEM; + + /* Allocate some space for the temporary string in which we will + construct filenames. */ + tmp = malloc (strlen (base_dir) + strlen (dir) + 100); + if (tmp == NULL) + return ENOMEM; + + + /* We make several passes through this loop. On the first pass, + we simply create the CVSADM directory in the deepest directory. + For each subsequent pass, we try to remove the last path + element from DIR, create the CVSADM directory in the remaining + pathname, and register the subdirectory in the newly created + CVSADM directory. */ + + retval = done = 0; + + strcpy (p, dir); + strcpy (dir_where_cvsadm_lives, base_dir); + strcat (dir_where_cvsadm_lives, "/"); + strcat (dir_where_cvsadm_lives, p); + dir_to_register = NULL; + + while (1) + { + /* Create CVSADM. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM); + if ((CVS_MKDIR (tmp, 0777) < 0) && (errno != EEXIST)) + { + retval = errno; + goto finish; + } + + /* Create CVSADM_REP. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM_REP); + if (! isfile (tmp)) + { + /* Use Emptydir as the placeholder until the client sends + us the real value. This code is similar to checkout.c + (emptydir_name), but the code below returns errors + differently. */ + + char *empty; + empty = malloc (strlen (CVSroot_directory) + + sizeof (CVSROOTADM) + + sizeof (CVSNULLREPOS) + + 10); + if (! empty) + { + retval = ENOMEM; + goto finish; + } + + /* Create the directory name. */ + (void) sprintf (empty, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSNULLREPOS); + + /* Create the directory if it doesn't exist. */ + if (! isfile (empty)) + { + mode_t omask; + omask = umask (cvsumask); + if (CVS_MKDIR (empty, 0777) < 0) + { + retval = errno; + free (empty); + goto finish; + } + (void) umask (omask); + } + + + f = CVS_FOPEN (tmp, "w"); + if (f == NULL) + { + retval = errno; + free (empty); + goto finish; + } + /* Write the directory name to CVSADM_REP. */ + if (fprintf (f, "%s\n", empty) < 0) + { + retval = errno; + fclose (f); + free (empty); + goto finish; + } + if (fclose (f) == EOF) + { + retval = errno; + free (empty); + goto finish; + } + + /* Clean up after ourselves. */ + free (empty); + } + + /* Create CVSADM_ENT. We open in append mode because we + don't want to clobber an existing Entries file. */ + (void) sprintf (tmp, "%s/%s", dir_where_cvsadm_lives, CVSADM_ENT); + f = CVS_FOPEN (tmp, "a"); + if (f == NULL) + { + retval = errno; + goto finish; + } + if (fclose (f) == EOF) + { + retval = errno; + goto finish; + } + + if (dir_to_register != NULL) + { + /* FIXME: Yes, this results in duplicate entries in the + Entries.Log file, but it doesn't currently matter. We + might need to change this later on to make sure that we + only write one entry. */ + + Subdir_Register ((List *) NULL, dir_where_cvsadm_lives, + dir_to_register); + } + + if (done) + break; + + dir_to_register = strrchr (p, '/'); + if (dir_to_register == NULL) + { + dir_to_register = p; + strcpy (dir_where_cvsadm_lives, base_dir); + done = 1; + } + else + { + *dir_to_register = '\0'; + dir_to_register++; + strcpy (dir_where_cvsadm_lives, base_dir); + strcat (dir_where_cvsadm_lives, "/"); + strcat (dir_where_cvsadm_lives, p); + } + } + + finish: + free (tmp); + free (dir_where_cvsadm_lives); + free (p); + return retval; +} + +/* + * Make directory DIR, including all intermediate directories if necessary. + * Returns 0 for success or errno code. + */ +static int mkdir_p PROTO((char *)); + +static int +mkdir_p (dir) + char *dir; +{ + char *p; + char *q = malloc (strlen (dir) + 1); + int retval; + + if (q == NULL) + return ENOMEM; + + retval = 0; + + /* + * Skip over leading slash if present. We won't bother to try to + * make '/'. + */ + p = dir + 1; + while (1) + { + while (*p != '/' && *p != '\0') + ++p; + if (*p == '/') + { + strncpy (q, dir, p - dir); + q[p - dir] = '\0'; + if (q[p - dir - 1] != '/' && CVS_MKDIR (q, 0777) < 0) + { + int saved_errno = errno; + + if (saved_errno != EEXIST + && ((saved_errno != EACCES && saved_errno != EROFS) + || !isdir (q))) + { + retval = saved_errno; + goto done; + } + } + ++p; + } + else + { + if (CVS_MKDIR (dir, 0777) < 0) + retval = errno; + goto done; + } + } + done: + free (q); + return retval; +} + +/* + * Print the error response for error code STATUS. The caller is + * reponsible for making sure we get back to the command loop without + * any further output occuring. + * Must be called only in contexts where it is OK to send output. + */ +static void +print_error (status) + int status; +{ + char *msg; + buf_output0 (buf_to_net, "error "); + msg = strerror (status); + if (msg) + buf_output0 (buf_to_net, msg); + buf_append_char (buf_to_net, '\n'); + + buf_flush (buf_to_net, 0); +} + +static int pending_error; +/* + * Malloc'd text for pending error. Each line must start with "E ". The + * last line should not end with a newline. + */ +static char *pending_error_text; + +/* If an error is pending, print it and return 1. If not, return 0. + Must be called only in contexts where it is OK to send output. */ +static int +print_pending_error () +{ + if (pending_error_text) + { + buf_output0 (buf_to_net, pending_error_text); + buf_append_char (buf_to_net, '\n'); + if (pending_error) + print_error (pending_error); + else + buf_output0 (buf_to_net, "error \n"); + + buf_flush (buf_to_net, 0); + + pending_error = 0; + free (pending_error_text); + pending_error_text = NULL; + return 1; + } + else if (pending_error) + { + print_error (pending_error); + pending_error = 0; + return 1; + } + else + return 0; +} + +/* Is an error pending? */ +#define error_pending() (pending_error || pending_error_text) + +static int alloc_pending PROTO ((size_t size)); + +/* Allocate SIZE bytes for pending_error_text and return nonzero + if we could do it. */ +static int +alloc_pending (size) + size_t size; +{ + if (error_pending ()) + /* Probably alloc_pending callers will have already checked for + this case. But we might as well handle it if they don't, I + guess. */ + return 0; + pending_error_text = malloc (size); + if (pending_error_text == NULL) + { + pending_error = ENOMEM; + return 0; + } + return 1; +} + +static void serve_is_modified PROTO ((char *)); + +static int supported_response PROTO ((char *)); + +static int +supported_response (name) + char *name; +{ + struct response *rs; + + for (rs = responses; rs->name != NULL; ++rs) + if (strcmp (rs->name, name) == 0) + return rs->status == rs_supported; + error (1, 0, "internal error: testing support for unknown response?"); + /* NOTREACHED */ + return 0; +} + +static void +serve_valid_responses (arg) + char *arg; +{ + char *p = arg; + char *q; + struct response *rs; + do + { + q = strchr (p, ' '); + if (q != NULL) + *q++ = '\0'; + for (rs = responses; rs->name != NULL; ++rs) + { + if (strcmp (rs->name, p) == 0) + break; + } + if (rs->name == NULL) + /* + * It is a response we have never heard of (and thus never + * will want to use). So don't worry about it. + */ + ; + else + rs->status = rs_supported; + p = q; + } while (q != NULL); + for (rs = responses; rs->name != NULL; ++rs) + { + if (rs->status == rs_essential) + { + buf_output0 (buf_to_net, "E response `"); + buf_output0 (buf_to_net, rs->name); + buf_output0 (buf_to_net, "' not supported by client\nerror \n"); + + /* FIXME: This call to buf_flush could conceivably + cause deadlock, as noted in server_cleanup. */ + buf_flush (buf_to_net, 1); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (EXIT_FAILURE); + } + else if (rs->status == rs_optional) + rs->status = rs_not_supported; + } +} + +static void +serve_root (arg) + char *arg; +{ + char *env; + char *path; + int save_errno; + char *arg_dup; + + if (error_pending()) return; + + if (!isabsolute (arg)) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Root %s must be an absolute pathname", arg); + return; + } + + /* Sending "Root" twice is illegal. + + The other way to handle a duplicate Root requests would be as a + request to clear out all state and start over as if it was a + new connection. Doing this would cause interoperability + headaches, so it should be a different request, if there is + any reason why such a feature is needed. */ + if (CVSroot_directory != NULL) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Protocol error: Duplicate Root request, for %s", arg); + return; + } + +#ifdef AUTH_SERVER_SUPPORT + if (Pserver_Repos != NULL) + { + if (strcmp (Pserver_Repos, arg) != 0) + { + if (alloc_pending (80 + strlen (Pserver_Repos) + strlen (arg))) + /* The explicitness is to aid people who are writing clients. + I don't see how this information could help an + attacker. */ + sprintf (pending_error_text, "\ +E Protocol error: Root says \"%s\" but pserver says \"%s\"", + arg, Pserver_Repos); + } + } +#endif + arg_dup = malloc (strlen (arg) + 1); + if (arg_dup == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (arg_dup, arg); + set_local_cvsroot (arg_dup); + + /* For pserver, this will already have happened, and the call will do + nothing. But for rsh, we need to do it now. */ + parse_config (CVSroot_directory); + + path = malloc (strlen (CVSroot_directory) + + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_HISTORY) + + 10); + if (path == NULL) + { + pending_error = ENOMEM; + return; + } + (void) sprintf (path, "%s/%s", CVSroot_directory, CVSROOTADM); + if (!isaccessible (path, R_OK | X_OK)) + { + save_errno = errno; + pending_error_text = malloc (80 + strlen (path)); + if (pending_error_text != NULL) + sprintf (pending_error_text, "E Cannot access %s", path); + pending_error = save_errno; + } + (void) strcat (path, "/"); + (void) strcat (path, CVSROOTADM_HISTORY); + if (isfile (path) && !isaccessible (path, R_OK | W_OK)) + { + save_errno = errno; + pending_error_text = malloc (80 + strlen (path)); + if (pending_error_text != NULL) + sprintf (pending_error_text, "E \ +Sorry, you don't have read/write access to the history file %s", path); + pending_error = save_errno; + } + free (path); + +#ifdef HAVE_PUTENV + env = malloc (strlen (CVSROOT_ENV) + strlen (CVSroot_directory) + 1 + 1); + if (env == NULL) + { + pending_error = ENOMEM; + return; + } + (void) sprintf (env, "%s=%s", CVSROOT_ENV, CVSroot_directory); + (void) putenv (env); + /* do not free env, as putenv has control of it */ +#endif +} + +static int max_dotdot_limit = 0; + +/* Is this pathname OK to recurse into when we are running as the server? + If not, call error() with a fatal error. */ +void +server_pathname_check (path) + char *path; +{ + /* An absolute pathname is almost surely a path on the *client* machine, + and is unlikely to do us any good here. It also is probably capable + of being a security hole in the anonymous readonly case. */ + if (isabsolute (path)) + /* Giving an error is actually kind of a cop-out, in the sense + that it would be nice for "cvs co -d /foo/bar/baz" to work. + A quick fix in the server would be requiring Max-dotdot of + at least one if pathnames are absolute, and then putting + /abs/foo/bar/baz in the temp dir beside the /d/d/d stuff. + A cleaner fix in the server might be to decouple the + pathnames we pass back to the client from pathnames in our + temp directory (this would also probably remove the need + for Max-dotdot). A fix in the client would have the client + turn it into "cd /foo/bar; cvs co -d baz" (more or less). + This probably has some problems with pathnames which appear + in messages. */ + error (1, 0, "absolute pathname `%s' illegal for server", path); + if (pathname_levels (path) > max_dotdot_limit) + { + /* Similar to the isabsolute case in security implications. */ + error (0, 0, "protocol error: `%s' contains more leading ..", path); + error (1, 0, "than the %d which Max-dotdot specified", + max_dotdot_limit); + } +} + +static int outside_root PROTO ((char *)); + +/* Is file or directory REPOS an absolute pathname within the + CVSroot_directory? If yes, return 0. If no, set pending_error + and return 1. */ +static int +outside_root (repos) + char *repos; +{ + size_t repos_len = strlen (repos); + size_t root_len = strlen (CVSroot_directory); + + /* I think isabsolute (repos) should always be true, and that + any RELATIVE_REPOS stuff should only be in CVS/Repository + files, not the protocol (for compatibility), but I'm putting + in the isabsolute check just in case. */ + if (!isabsolute (repos)) + { + if (alloc_pending (repos_len + 80)) + sprintf (pending_error_text, "\ +E protocol error: %s is not absolute", repos); + return 1; + } + + if (repos_len < root_len + || strncmp (CVSroot_directory, repos, root_len) != 0) + { + not_within: + if (alloc_pending (strlen (CVSroot_directory) + + strlen (repos) + + 80)) + sprintf (pending_error_text, "\ +E protocol error: directory '%s' not within root '%s'", + repos, CVSroot_directory); + return 1; + } + if (repos_len > root_len) + { + if (repos[root_len] != '/') + goto not_within; + if (pathname_levels (repos + root_len + 1) > 0) + goto not_within; + } + return 0; +} + +static int outside_dir PROTO ((char *)); + +/* Is file or directory FILE outside the current directory (that is, does + it contain '/')? If no, return 0. If yes, set pending_error + and return 1. */ +static int +outside_dir (file) + char *file; +{ + if (strchr (file, '/') != NULL) + { + if (alloc_pending (strlen (file) + + 80)) + sprintf (pending_error_text, "\ +E protocol error: directory '%s' not within current directory", + file); + return 1; + } + return 0; +} + +/* + * Add as many directories to the temp directory as the client tells us it + * will use "..", so we never try to access something outside the temp + * directory via "..". + */ +static void +serve_max_dotdot (arg) + char *arg; +{ + int lim = atoi (arg); + int i; + char *p; + + if (lim < 0) + return; + p = malloc (strlen (server_temp_dir) + 2 * lim + 10); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (p, server_temp_dir); + for (i = 0; i < lim; ++i) + strcat (p, "/d"); + if (server_temp_dir != orig_server_temp_dir) + free (server_temp_dir); + server_temp_dir = p; + max_dotdot_limit = lim; +} + +static char *dir_name; + +static void +dirswitch (dir, repos) + char *dir; + char *repos; +{ + int status; + FILE *f; + size_t dir_len; + + server_write_entries (); + + if (error_pending()) return; + + /* Check for bad directory name. + + FIXME: could/should unify these checks with server_pathname_check + except they need to report errors differently. */ + if (isabsolute (dir)) + { + if (alloc_pending (80 + strlen (dir))) + sprintf (pending_error_text, + "E absolute pathname `%s' illegal for server", dir); + return; + } + if (pathname_levels (dir) > max_dotdot_limit) + { + if (alloc_pending (80 + strlen (dir))) + sprintf (pending_error_text, + "E protocol error: `%s' has too many ..", dir); + return; + } + + if (dir_name != NULL) + free (dir_name); + + dir_len = strlen (dir); + + /* Check for a trailing '/'. This is not ISDIRSEP because \ in the + protocol is an ordinary character, not a directory separator (of + course, it is perhaps unwise to use it in directory names, but that + is another issue). */ + if (dir_len > 0 + && dir[dir_len - 1] == '/') + { + if (alloc_pending (80 + dir_len)) + sprintf (pending_error_text, + "E protocol error: invalid directory syntax in %s", dir); + return; + } + + dir_name = malloc (strlen (server_temp_dir) + dir_len + 40); + if (dir_name == NULL) + { + pending_error = ENOMEM; + return; + } + + strcpy (dir_name, server_temp_dir); + strcat (dir_name, "/"); + strcat (dir_name, dir); + + status = mkdir_p (dir_name); + if (status != 0 + && status != EEXIST) + { + pending_error = status; + if (alloc_pending (80 + strlen (dir_name))) + sprintf (pending_error_text, "E cannot mkdir %s", dir_name); + return; + } + + /* We need to create adm directories in all path elements because + we want the server to descend them, even if the client hasn't + sent the appropriate "Argument xxx" command to match the + already-sent "Directory xxx" command. See recurse.c + (start_recursion) for a big discussion of this. */ + + status = create_adm_p (server_temp_dir, dir); + if (status != 0) + { + pending_error = status; + if (alloc_pending (80 + strlen (dir_name))) + sprintf (pending_error_text, "E cannot create_adm_p %s", dir_name); + return; + } + + if ( CVS_CHDIR (dir_name) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen (dir_name))) + sprintf (pending_error_text, "E cannot change to %s", dir_name); + return; + } + /* + * This is pretty much like calling Create_Admin, but Create_Admin doesn't + * report errors in the right way for us. + */ + if ((CVS_MKDIR (CVSADM, 0777) < 0) && (errno != EEXIST)) + { + pending_error = errno; + return; + } + + /* The following will overwrite the contents of CVSADM_REP. This + is the correct behavior -- mkdir_p may have written a + placeholder value to this file and we need to insert the + correct value. */ + + f = CVS_FOPEN (CVSADM_REP, "w"); + if (f == NULL) + { + pending_error = errno; + return; + } + if (fprintf (f, "%s", repos) < 0) + { + pending_error = errno; + fclose (f); + return; + } + /* Non-remote CVS handles a module representing the entire tree + (e.g., an entry like ``world -a .'') by putting /. at the end + of the Repository file, so we do the same. */ + if (strcmp (dir, ".") == 0 + && CVSroot_directory != NULL + && strcmp (CVSroot_directory, repos) == 0) + { + if (fprintf (f, "/.") < 0) + { + pending_error = errno; + fclose (f); + return; + } + } + if (fprintf (f, "\n") < 0) + { + pending_error = errno; + fclose (f); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + return; + } + /* We open in append mode because we don't want to clobber an + existing Entries file. */ + f = CVS_FOPEN (CVSADM_ENT, "a"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENT); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENT); + return; + } +} + +static void +serve_repository (arg) + char *arg; +{ + pending_error_text = malloc (80); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + strcpy (pending_error_text, + "E Repository request is obsolete; aborted"); + return; +} + +static void +serve_directory (arg) + char *arg; +{ + int status; + char *repos; + + status = buf_read_line (buf_from_net, &repos, (int *) NULL); + if (status == 0) + { + if (outside_root (repos)) + return; + dirswitch (arg, repos); + free (repos); + } + else if (status == -2) + { + pending_error = ENOMEM; + } + else + { + pending_error_text = malloc (80 + strlen (arg)); + if (pending_error_text == NULL) + { + pending_error = ENOMEM; + } + else if (status == -1) + { + sprintf (pending_error_text, + "E end of file reading mode for %s", arg); + } + else + { + sprintf (pending_error_text, + "E error reading mode for %s", arg); + pending_error = status; + } + } +} + +static void +serve_static_directory (arg) + char *arg; +{ + FILE *f; + + if (error_pending ()) return; + + f = CVS_FOPEN (CVSADM_ENTSTAT, "w+"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENTSTAT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENTSTAT); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENTSTAT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENTSTAT); + return; + } +} + +static void +serve_sticky (arg) + char *arg; +{ + FILE *f; + + if (error_pending ()) return; + + f = CVS_FOPEN (CVSADM_TAG, "w+"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_TAG); + return; + } + if (fprintf (f, "%s\n", arg) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot write to %s", CVSADM_TAG); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_TAG))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_TAG); + return; + } +} + +/* + * Read SIZE bytes from buf_from_net, write them to FILE. + * + * Currently this isn't really used for receiving parts of a file -- + * the file is still sent over in one chunk. But if/when we get + * spiffy in-process gzip support working, perhaps the compressed + * pieces could be sent over as they're ready, if the network is fast + * enough. Or something. + */ +static void +receive_partial_file (size, file) + int size; + int file; +{ + while (size > 0) + { + int status, nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = malloc (80); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else if (status == -1) + { + sprintf (pending_error_text, + "E premature end of file from client"); + pending_error = 0; + } + else + { + sprintf (pending_error_text, + "E error reading from client"); + pending_error = status; + } + } + return; + } + + size -= nread; + + while (nread > 0) + { + int nwrote; + + nwrote = write (file, data, nread); + if (nwrote < 0) + { + pending_error_text = malloc (40); + if (pending_error_text != NULL) + sprintf (pending_error_text, "E unable to write"); + pending_error = errno; + + /* Read and discard the file data. */ + while (size > 0) + { + int status, nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + return; + size -= nread; + } + + return; + } + nread -= nwrote; + data += nwrote; + } + } +} + +/* Receive SIZE bytes, write to filename FILE. */ +static void +receive_file (size, file, gzipped) + int size; + char *file; + int gzipped; +{ + int fd; + char *arg = file; + + /* Write the file. */ + fd = CVS_OPEN (arg, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + { + pending_error_text = malloc (40 + strlen (arg)); + if (pending_error_text) + sprintf (pending_error_text, "E cannot open %s", arg); + pending_error = errno; + return; + } + + if (gzipped) + { + /* Using gunzip_and_write isn't really a high-performance + approach, because it keeps the whole thing in memory + (contiguous memory, worse yet). But it seems easier to + code than the alternative (and less vulnerable to subtle + bugs). Given that this feature is mainly for + compatibility, that is the better tradeoff. */ + + int toread = size; + char *filebuf; + char *p; + + filebuf = malloc (size); + p = filebuf; + /* If NULL, we still want to read the data and discard it. */ + + while (toread > 0) + { + int status, nread; + char *data; + + status = buf_read_data (buf_from_net, toread, &data, &nread); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = malloc (80); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else if (status == -1) + { + sprintf (pending_error_text, + "E premature end of file from client"); + pending_error = 0; + } + else + { + sprintf (pending_error_text, + "E error reading from client"); + pending_error = status; + } + } + return; + } + + toread -= nread; + + if (filebuf != NULL) + { + memcpy (p, data, nread); + p += nread; + } + } + if (filebuf == NULL) + { + pending_error = ENOMEM; + goto out; + } + + if (gunzip_and_write (fd, file, filebuf, size)) + { + if (alloc_pending (80)) + sprintf (pending_error_text, + "E aborting due to compression error"); + } + free (filebuf); + } + else + receive_partial_file (size, fd); + + if (pending_error_text) + { + char *p = realloc (pending_error_text, + strlen (pending_error_text) + strlen (arg) + 30); + if (p) + { + pending_error_text = p; + sprintf (p + strlen (p), ", file %s", arg); + } + /* else original string is supposed to be unchanged */ + } + + out: + if (close (fd) < 0 && !error_pending ()) + { + pending_error_text = malloc (40 + strlen (arg)); + if (pending_error_text) + sprintf (pending_error_text, "E cannot close %s", arg); + pending_error = errno; + return; + } +} + +/* Kopt for the next file sent in Modified or Is-modified. */ +static char *kopt; + +/* Timestamp (Checkin-time) for next file sent in Modified or + Is-modified. */ +static int checkin_time_valid; +static time_t checkin_time; + +static void serve_modified PROTO ((char *)); + +static void +serve_modified (arg) + char *arg; +{ + int size, status; + char *size_text; + char *mode_text; + + int gzipped = 0; + + /* + * This used to return immediately if error_pending () was true. + * However, that fails, because it causes each line of the file to + * be echoed back to the client as an unrecognized command. The + * client isn't reading from the socket, so eventually both + * processes block trying to write to the other. Now, we try to + * read the file if we can. + */ + + status = buf_read_line (buf_from_net, &mode_text, (int *) NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = malloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading mode for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading mode for %s", arg); + pending_error = status; + } + } + } + return; + } + + status = buf_read_line (buf_from_net, &size_text, (int *) NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = malloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading size for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading size for %s", arg); + pending_error = errno; + } + } + } + return; + } + if (size_text[0] == 'z') + { + gzipped = 1; + size = atoi (size_text + 1); + } + else + size = atoi (size_text); + free (size_text); + + if (error_pending ()) + { + /* Now that we know the size, read and discard the file data. */ + while (size > 0) + { + int status, nread; + char *data; + + status = buf_read_data (buf_from_net, size, &data, &nread); + if (status != 0) + return; + size -= nread; + } + return; + } + + if (outside_dir (arg)) + return; + + if (size >= 0) + { + receive_file (size, arg, gzipped); + if (error_pending ()) return; + } + + if (checkin_time_valid) + { + struct utimbuf t; + + memset (&t, 0, sizeof (t)); + t.modtime = t.actime = checkin_time; + if (utime (arg, &t) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, "E cannot utime %s", arg); + return; + } + checkin_time_valid = 0; + } + + { + int status = change_mode (arg, mode_text, 0); + free (mode_text); + if (status) + { + pending_error_text = malloc (40 + strlen (arg)); + if (pending_error_text) + sprintf (pending_error_text, + "E cannot change mode for %s", arg); + pending_error = status; + return; + } + } + + /* Make sure that the Entries indicate the right kopt. We probably + could do this even in the non-kopt case and, I think, save a stat() + call in time_stamp_server. But for conservatism I'm leaving the + non-kopt case alone. */ + if (kopt != NULL) + serve_is_modified (arg); +} + + +static void +serve_enable_unchanged (arg) + char *arg; +{ +} + +struct an_entry { + struct an_entry *next; + char *entry; +}; + +static struct an_entry *entries; + +static void serve_unchanged PROTO ((char *)); + +static void +serve_unchanged (arg) + char *arg; +{ + struct an_entry *p; + char *name; + char *cp; + char *timefield; + + if (error_pending ()) + return; + + if (outside_dir (arg)) + return; + + /* Rewrite entries file to have `=' in timestamp field. */ + for (p = entries; p != NULL; p = p->next) + { + name = p->entry + 1; + cp = strchr (name, '/'); + if (cp != NULL + && strlen (arg) == cp - name + && strncmp (arg, name, cp - name) == 0) + { + timefield = strchr (cp + 1, '/') + 1; + if (*timefield != '=') + { + cp = timefield + strlen (timefield); + cp[1] = '\0'; + while (cp > timefield) + { + *cp = cp[-1]; + --cp; + } + *timefield = '='; + } + break; + } + } +} + +static void +serve_is_modified (arg) + char *arg; +{ + struct an_entry *p; + char *name; + char *cp; + char *timefield; + /* Have we found this file in "entries" yet. */ + int found; + + if (error_pending ()) + return; + + if (outside_dir (arg)) + return; + + /* Rewrite entries file to have `M' in timestamp field. */ + found = 0; + for (p = entries; p != NULL; p = p->next) + { + name = p->entry + 1; + cp = strchr (name, '/'); + if (cp != NULL + && strlen (arg) == cp - name + && strncmp (arg, name, cp - name) == 0) + { + timefield = strchr (cp + 1, '/') + 1; + if (!(timefield[0] == 'M' && timefield[1] == '/')) + { + cp = timefield + strlen (timefield); + cp[1] = '\0'; + while (cp > timefield) + { + *cp = cp[-1]; + --cp; + } + *timefield = 'M'; + } + if (kopt != NULL) + { + if (alloc_pending (strlen (name) + 80)) + sprintf (pending_error_text, + "E protocol error: both Kopt and Entry for %s", + arg); + free (kopt); + kopt = NULL; + return; + } + found = 1; + break; + } + } + if (!found) + { + /* We got Is-modified but no Entry. Add a dummy entry. + The "D" timestamp is what makes it a dummy. */ + p = (struct an_entry *) malloc (sizeof (struct an_entry)); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + p->entry = malloc (strlen (arg) + 80); + if (p->entry == NULL) + { + pending_error = ENOMEM; + free (p); + return; + } + strcpy (p->entry, "/"); + strcat (p->entry, arg); + strcat (p->entry, "//D/"); + if (kopt != NULL) + { + strcat (p->entry, kopt); + free (kopt); + kopt = NULL; + } + strcat (p->entry, "/"); + p->next = entries; + entries = p; + } +} + +static void serve_entry PROTO ((char *)); + +static void +serve_entry (arg) + char *arg; +{ + struct an_entry *p; + char *cp; + if (error_pending()) return; + p = (struct an_entry *) malloc (sizeof (struct an_entry)); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + /* Leave space for serve_unchanged to write '=' if it wants. */ + cp = malloc (strlen (arg) + 2); + if (cp == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (cp, arg); + p->next = entries; + p->entry = cp; + entries = p; +} + +static void serve_kopt PROTO ((char *)); + +static void +serve_kopt (arg) + char *arg; +{ + if (error_pending ()) + return; + + if (kopt != NULL) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: duplicate Kopt request: %s", arg); + return; + } + + /* Do some sanity checks. In particular, that it is not too long. + This lets the rest of the code not worry so much about buffer + overrun attacks. Probably should call RCS_check_kflag here, + but that would mean changing RCS_check_kflag to handle errors + other than via exit(), fprintf(), and such. */ + if (strlen (arg) > 10) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: invalid Kopt request: %s", arg); + return; + } + + kopt = malloc (strlen (arg) + 1); + if (kopt == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (kopt, arg); +} + +static void serve_checkin_time PROTO ((char *)); + +static void +serve_checkin_time (arg) + char *arg; +{ + if (error_pending ()) + return; + + if (checkin_time_valid) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E protocol error: duplicate Checkin-time request: %s", + arg); + return; + } + + checkin_time = get_date (arg, NULL); + if (checkin_time == (time_t)-1) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, "E cannot parse date %s", arg); + return; + } + checkin_time_valid = 1; +} + +static void +server_write_entries () +{ + FILE *f; + struct an_entry *p; + struct an_entry *q; + + if (entries == NULL) + return; + + f = NULL; + /* Note that we free all the entries regardless of errors. */ + if (!error_pending ()) + { + /* We open in append mode because we don't want to clobber an + existing Entries file. If we are checking out a module + which explicitly lists more than one file in a particular + directory, then we will wind up calling + server_write_entries for each such file. */ + f = CVS_FOPEN (CVSADM_ENT, "a"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_ENT); + } + } + for (p = entries; p != NULL;) + { + if (!error_pending ()) + { + if (fprintf (f, "%s\n", p->entry) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen(CVSADM_ENT))) + sprintf (pending_error_text, + "E cannot write to %s", CVSADM_ENT); + } + } + free (p->entry); + q = p->next; + free (p); + p = q; + } + entries = NULL; + if (f != NULL && fclose (f) == EOF && !error_pending ()) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_ENT))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_ENT); + } +} + +struct notify_note { + /* Directory in which this notification happens. malloc'd*/ + char *dir; + + /* malloc'd. */ + char *filename; + + /* The following three all in one malloc'd block, pointed to by TYPE. + Each '\0' terminated. */ + /* "E" or "U". */ + char *type; + /* time+host+dir */ + char *val; + char *watches; + + struct notify_note *next; +}; + +static struct notify_note *notify_list; +/* Used while building list, to point to the last node that already exists. */ +static struct notify_note *last_node; + +static void serve_notify PROTO ((char *)); + +static void +serve_notify (arg) + char *arg; +{ + struct notify_note *new; + char *data; + int status; + + if (error_pending ()) return; + + if (outside_dir (arg)) + return; + + new = (struct notify_note *) malloc (sizeof (struct notify_note)); + if (new == NULL) + { + pending_error = ENOMEM; + return; + } + if (dir_name == NULL) + goto error; + new->dir = malloc (strlen (dir_name) + 1); + if (new->dir == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (new->dir, dir_name); + new->filename = malloc (strlen (arg) + 1); + if (new->filename == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (new->filename, arg); + + status = buf_read_line (buf_from_net, &data, (int *) NULL); + if (status != 0) + { + if (status == -2) + pending_error = ENOMEM; + else + { + pending_error_text = malloc (80 + strlen (arg)); + if (pending_error_text == NULL) + pending_error = ENOMEM; + else + { + if (status == -1) + sprintf (pending_error_text, + "E end of file reading notification for %s", arg); + else + { + sprintf (pending_error_text, + "E error reading notification for %s", arg); + pending_error = status; + } + } + } + } + else + { + char *cp; + + if (strchr (data, '+')) + goto error; + + new->type = data; + if (data[1] != '\t') + goto error; + data[1] = '\0'; + cp = data + 2; + new->val = cp; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '+'; + cp = strchr (cp, '\t'); + if (cp == NULL) + goto error; + *cp++ = '\0'; + new->watches = cp; + /* If there is another tab, ignore everything after it, + for future expansion. */ + cp = strchr (cp, '\t'); + if (cp != NULL) + { + *cp = '\0'; + } + + new->next = NULL; + + if (last_node == NULL) + { + notify_list = new; + } + else + last_node->next = new; + last_node = new; + } + return; + error: + pending_error_text = malloc (80); + if (pending_error_text) + strcpy (pending_error_text, + "E Protocol error; misformed Notify request"); + pending_error = 0; + return; +} + +/* Process all the Notify requests that we have stored up. Returns 0 + if successful, if not prints error message (via error()) and + returns negative value. */ +static int +server_notify () +{ + struct notify_note *p; + char *repos; + + while (notify_list != NULL) + { + if ( CVS_CHDIR (notify_list->dir) < 0) + { + error (0, errno, "cannot change to %s", notify_list->dir); + return -1; + } + repos = Name_Repository (NULL, NULL); + + lock_dir_for_write (repos); + + fileattr_startdir (repos); + + notify_do (*notify_list->type, notify_list->filename, getcaller(), + notify_list->val, notify_list->watches, repos); + + buf_output0 (buf_to_net, "Notified "); + { + char *dir = notify_list->dir + strlen (server_temp_dir) + 1; + if (dir[0] == '\0') + buf_append_char (buf_to_net, '.'); + else + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '/'); + buf_append_char (buf_to_net, '\n'); + } + buf_output0 (buf_to_net, repos); + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, notify_list->filename); + buf_append_char (buf_to_net, '\n'); + + p = notify_list->next; + free (notify_list->filename); + free (notify_list->dir); + free (notify_list->type); + free (notify_list); + notify_list = p; + + fileattr_write (); + fileattr_free (); + + Lock_Cleanup (); + } + + last_node = NULL; + + /* The code used to call fflush (stdout) here, but that is no + longer necessary. The data is now buffered in buf_to_net, + which will be flushed by the caller, do_cvs_command. */ + + return 0; +} + +static int argument_count; +static char **argument_vector; +static int argument_vector_size; + +static void +serve_argument (arg) + char *arg; +{ + char *p; + + if (error_pending()) return; + + if (argument_vector_size <= argument_count) + { + argument_vector_size *= 2; + argument_vector = + (char **) realloc ((char *)argument_vector, + argument_vector_size * sizeof (char *)); + if (argument_vector == NULL) + { + pending_error = ENOMEM; + return; + } + } + p = malloc (strlen (arg) + 1); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcpy (p, arg); + argument_vector[argument_count++] = p; +} + +static void +serve_argumentx (arg) + char *arg; +{ + char *p; + + if (error_pending()) return; + + p = argument_vector[argument_count - 1]; + p = realloc (p, strlen (p) + 1 + strlen (arg) + 1); + if (p == NULL) + { + pending_error = ENOMEM; + return; + } + strcat (p, "\n"); + strcat (p, arg); + argument_vector[argument_count - 1] = p; +} + +static void +serve_global_option (arg) + char *arg; +{ + if (arg[0] != '-' || arg[1] == '\0' || arg[2] != '\0') + { + error_return: + if (alloc_pending (strlen (arg) + 80)) + sprintf (pending_error_text, + "E Protocol error: bad global option %s", + arg); + return; + } + switch (arg[1]) + { + case 'n': + noexec = 1; + break; + case 'q': + quiet = 1; + break; + case 'r': + cvswrite = 0; + break; + case 'Q': + really_quiet = 1; + break; + case 'l': + logoff = 1; + break; + case 't': + trace = 1; + break; + default: + goto error_return; + } +} + +static void +serve_set (arg) + char *arg; +{ + /* FIXME: This sends errors immediately (I think); they should be + put into pending_error. */ + variable_set (arg); +} + +#ifdef ENCRYPTION + +#ifdef HAVE_KERBEROS + +static void +serve_kerberos_encrypt (arg) + char *arg; +{ + /* All future communication with the client will be encrypted. */ + + buf_to_net = krb_encrypt_buffer_initialize (buf_to_net, 0, sched, + kblock, + buf_to_net->memory_error); + buf_from_net = krb_encrypt_buffer_initialize (buf_from_net, 1, sched, + kblock, + buf_from_net->memory_error); +} + +#endif /* HAVE_KERBEROS */ + +#ifdef HAVE_GSSAPI + +static void +serve_gssapi_encrypt (arg) + char *arg; +{ + if (cvs_gssapi_wrapping) + { + /* We're already using a gssapi_wrap buffer for stream + authentication. Flush everything we've output so far, and + turn on encryption for future data. On the input side, we + should only have unwrapped as far as the Gssapi-encrypt + command, so future unwrapping will become encrypted. */ + buf_flush (buf_to_net, 1); + cvs_gssapi_encrypt = 1; + return; + } + + /* All future communication with the client will be encrypted. */ + + cvs_gssapi_encrypt = 1; + + buf_to_net = cvs_gssapi_wrap_buffer_initialize (buf_to_net, 0, + gcontext, + buf_to_net->memory_error); + buf_from_net = cvs_gssapi_wrap_buffer_initialize (buf_from_net, 1, + gcontext, + buf_from_net->memory_error); + + cvs_gssapi_wrapping = 1; +} + +#endif /* HAVE_GSSAPI */ + +#endif /* ENCRYPTION */ + +#ifdef HAVE_GSSAPI + +static void +serve_gssapi_authenticate (arg) + char *arg; +{ + if (cvs_gssapi_wrapping) + { + /* We're already using a gssapi_wrap buffer for encryption. + That includes authentication, so we don't have to do + anything further. */ + return; + } + + buf_to_net = cvs_gssapi_wrap_buffer_initialize (buf_to_net, 0, + gcontext, + buf_to_net->memory_error); + buf_from_net = cvs_gssapi_wrap_buffer_initialize (buf_from_net, 1, + gcontext, + buf_from_net->memory_error); + + cvs_gssapi_wrapping = 1; +} + +#endif /* HAVE_GSSAPI */ + +#ifdef SERVER_FLOWCONTROL +/* The maximum we'll queue to the remote client before blocking. */ +# ifndef SERVER_HI_WATER +# define SERVER_HI_WATER (2 * 1024 * 1024) +# endif /* SERVER_HI_WATER */ +/* When the buffer drops to this, we restart the child */ +# ifndef SERVER_LO_WATER +# define SERVER_LO_WATER (1 * 1024 * 1024) +# endif /* SERVER_LO_WATER */ + +static int set_nonblock_fd PROTO((int)); + +/* + * Set buffer BUF to non-blocking I/O. Returns 0 for success or errno + * code. + */ + +static int +set_nonblock_fd (fd) + int fd; +{ + int flags; + + flags = fcntl (fd, F_GETFL, 0); + if (flags < 0) + return errno; + if (fcntl (fd, F_SETFL, flags | O_NONBLOCK) < 0) + return errno; + return 0; +} + +#endif /* SERVER_FLOWCONTROL */ + +static void serve_questionable PROTO((char *)); + +static void +serve_questionable (arg) + char *arg; +{ + static int initted; + + if (!initted) + { + /* Pick up ignores from CVSROOTADM_IGNORE, $HOME/.cvsignore on server, + and CVSIGNORE on server. */ + ign_setup (); + initted = 1; + } + + if (dir_name == NULL) + { + buf_output0 (buf_to_net, "E Protocol error: 'Directory' missing"); + return; + } + + if (outside_dir (arg)) + return; + + if (!ign_name (arg)) + { + char *update_dir; + + buf_output (buf_to_net, "M ? ", 4); + update_dir = dir_name + strlen (server_temp_dir) + 1; + if (!(update_dir[0] == '.' && update_dir[1] == '\0')) + { + buf_output0 (buf_to_net, update_dir); + buf_output (buf_to_net, "/", 1); + } + buf_output0 (buf_to_net, arg); + buf_output (buf_to_net, "\n", 1); + } +} + +static void serve_case PROTO ((char *)); + +static void +serve_case (arg) + char *arg; +{ + ign_case = 1; +} + +static struct buffer *protocol; + +/* This is the output which we are saving up to send to the server, in the + child process. We will push it through, via the `protocol' buffer, when + we have a complete line. */ +static struct buffer *saved_output; +/* Likewise, but stuff which will go to stderr. */ +static struct buffer *saved_outerr; + +static void +protocol_memory_error (buf) + struct buffer *buf; +{ + error (1, ENOMEM, "Virtual memory exhausted"); +} + +/* + * Process IDs of the subprocess, or negative if that subprocess + * does not exist. + */ +static pid_t command_pid; + +static void +outbuf_memory_error (buf) + struct buffer *buf; +{ + static const char msg[] = "E Fatal server error\n\ +error ENOMEM Virtual memory exhausted.\n"; + if (command_pid > 0) + kill (command_pid, SIGTERM); + + /* + * We have arranged things so that printing this now either will + * be legal, or the "E fatal error" line will get glommed onto the + * end of an existing "E" or "M" response. + */ + + /* If this gives an error, not much we could do. syslog() it? */ + write (STDOUT_FILENO, msg, sizeof (msg) - 1); + error_exit (); +} + +static void +input_memory_error (buf) + struct buffer *buf; +{ + outbuf_memory_error (buf); +} + + + +/* If command is legal, return 1. + * Else if command is illegal and croak_on_illegal is set, then die. + * Else just return 0 to indicate that command is illegal. + */ +static int +check_command_legal_p (cmd_name) + char *cmd_name; +{ + /* Right now, only pserver notices illegal commands -- namely, + * write attempts by a read-only user. Therefore, if CVS_Username + * is not set, this just returns 1, because CVS_Username unset + * means pserver is not active. + */ +#ifdef AUTH_SERVER_SUPPORT + if (CVS_Username == NULL) + return 1; + + if (lookup_command_attribute (cmd_name) & CVS_CMD_MODIFIES_REPOSITORY) + { + /* This command has the potential to modify the repository, so + * we check if the user have permission to do that. + * + * (Only relevant for remote users -- local users can do + * whatever normal Unix file permissions allow them to do.) + * + * The decision method: + * + * If $CVSROOT/CVSADMROOT_READERS exists and user is listed + * in it, then read-only access for user. + * + * Or if $CVSROOT/CVSADMROOT_WRITERS exists and user NOT + * listed in it, then also read-only access for user. + * + * Else read-write access for user. + */ + + char *linebuf = NULL; + int num_red = 0; + size_t linebuf_len = 0; + char *fname; + size_t flen; + FILE *fp; + int found_it = 0; + + /* else */ + flen = strlen (CVSroot_directory) + + strlen (CVSROOTADM) + + strlen (CVSROOTADM_READERS) + + 3; + + fname = xmalloc (flen); + (void) sprintf (fname, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_READERS); + + fp = fopen (fname, "r"); + + if (fp == NULL) + { + if (!existence_error (errno)) + { + /* Need to deny access, so that attackers can't fool + us with some sort of denial of service attack. */ + error (0, errno, "cannot open %s", fname); + free (fname); + return 0; + } + } + else /* successfully opened readers file */ + { + while ((num_red = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + /* Hmmm, is it worth importing my own readline + library into CVS? It takes care of chopping + leading and trailing whitespace, "#" comments, and + newlines automatically when so requested. Would + save some code here... -kff */ + + /* Chop newline by hand, for strcmp()'s sake. */ + if (linebuf[num_red - 1] == '\n') + linebuf[num_red - 1] = '\0'; + + if (strcmp (linebuf, CVS_Username) == 0) + goto handle_illegal; + } + if (num_red < 0 && !feof (fp)) + error (0, errno, "cannot read %s", fname); + + /* If not listed specifically as a reader, then this user + has write access by default unless writers are also + specified in a file . */ + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + } + free (fname); + + /* Now check the writers file. */ + + flen = strlen (CVSroot_directory) + + strlen (CVSROOTADM) + + strlen (CVSROOTADM_WRITERS) + + 3; + + fname = xmalloc (flen); + (void) sprintf (fname, "%s/%s/%s", CVSroot_directory, + CVSROOTADM, CVSROOTADM_WRITERS); + + fp = fopen (fname, "r"); + + if (fp == NULL) + { + if (linebuf) + free (linebuf); + if (existence_error (errno)) + { + /* Writers file does not exist, so everyone is a writer, + by default. */ + free (fname); + return 1; + } + else + { + /* Need to deny access, so that attackers can't fool + us with some sort of denial of service attack. */ + error (0, errno, "cannot read %s", fname); + free (fname); + return 0; + } + } + + found_it = 0; + while ((num_red = getline (&linebuf, &linebuf_len, fp)) >= 0) + { + /* Chop newline by hand, for strcmp()'s sake. */ + if (linebuf[num_red - 1] == '\n') + linebuf[num_red - 1] = '\0'; + + if (strcmp (linebuf, CVS_Username) == 0) + { + found_it = 1; + break; + } + } + if (num_red < 0 && !feof (fp)) + error (0, errno, "cannot read %s", fname); + + if (found_it) + { + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + if (linebuf) + free (linebuf); + free (fname); + return 1; + } + else /* writers file exists, but this user not listed in it */ + { + handle_illegal: + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", fname); + if (linebuf) + free (linebuf); + free (fname); + return 0; + } + } +#endif /* AUTH_SERVER_SUPPORT */ + + /* If ever reach end of this function, command must be legal. */ + return 1; +} + + + +/* Execute COMMAND in a subprocess with the approriate funky things done. */ + +static struct fd_set_wrapper { fd_set fds; } command_fds_to_drain; +static int max_command_fd; + +#ifdef SERVER_FLOWCONTROL +static int flowcontrol_pipe[2]; +#endif /* SERVER_FLOWCONTROL */ + +static void +do_cvs_command (cmd_name, command) + char *cmd_name; + int (*command) PROTO((int argc, char **argv)); +{ + /* + * The following file descriptors are set to -1 if that file is not + * currently open. + */ + + /* Data on these pipes is a series of '\n'-terminated lines. */ + int stdout_pipe[2]; + int stderr_pipe[2]; + + /* + * Data on this pipe is a series of counted (see buf_send_counted) + * packets. Each packet must be processed atomically (i.e. not + * interleaved with data from stdout_pipe or stderr_pipe). + */ + int protocol_pipe[2]; + + int dev_null_fd = -1; + + int errs; + + command_pid = -1; + stdout_pipe[0] = -1; + stdout_pipe[1] = -1; + stderr_pipe[0] = -1; + stderr_pipe[1] = -1; + protocol_pipe[0] = -1; + protocol_pipe[1] = -1; + + server_write_entries (); + + if (print_pending_error ()) + goto free_args_and_return; + + /* Global `command_name' is probably "server" right now -- only + serve_export() sets it to anything else. So we will use local + parameter `cmd_name' to determine if this command is legal for + this user. */ + if (!check_command_legal_p (cmd_name)) + { + buf_output0 (buf_to_net, "E "); + buf_output0 (buf_to_net, program_name); + buf_output0 (buf_to_net, " [server aborted]: \""); + buf_output0 (buf_to_net, cmd_name); + buf_output0 (buf_to_net, "\" requires write access to the repository\n\ +error \n"); + goto free_args_and_return; + } + + (void) server_notify (); + + /* + * We use a child process which actually does the operation. This + * is so we can intercept its standard output. Even if all of CVS + * were written to go to some special routine instead of writing + * to stdout or stderr, we would still need to do the same thing + * for the RCS commands. + */ + + if (pipe (stdout_pipe) < 0) + { + print_error (errno); + goto error_exit; + } + if (pipe (stderr_pipe) < 0) + { + print_error (errno); + goto error_exit; + } + if (pipe (protocol_pipe) < 0) + { + print_error (errno); + goto error_exit; + } +#ifdef SERVER_FLOWCONTROL + if (pipe (flowcontrol_pipe) < 0) + { + print_error (errno); + goto error_exit; + } + set_nonblock_fd (flowcontrol_pipe[0]); + set_nonblock_fd (flowcontrol_pipe[1]); +#endif /* SERVER_FLOWCONTROL */ + + dev_null_fd = CVS_OPEN (DEVNULL, O_RDONLY); + if (dev_null_fd < 0) + { + print_error (errno); + goto error_exit; + } + + /* We shouldn't have any partial lines from cvs_output and + cvs_outerr, but we handle them here in case there is a bug. */ + /* FIXME: appending a newline, rather than using "MT" as we + do in the child process, is probably not really a very good + way to "handle" them. */ + if (! buf_empty_p (saved_output)) + { + buf_append_char (saved_output, '\n'); + buf_copy_lines (buf_to_net, saved_output, 'M'); + } + if (! buf_empty_p (saved_outerr)) + { + buf_append_char (saved_outerr, '\n'); + buf_copy_lines (buf_to_net, saved_outerr, 'E'); + } + + /* Flush out any pending data. */ + buf_flush (buf_to_net, 1); + + /* Don't use vfork; we're not going to exec(). */ + command_pid = fork (); + if (command_pid < 0) + { + print_error (errno); + goto error_exit; + } + if (command_pid == 0) + { + int exitstatus; + + /* Since we're in the child, and the parent is going to take + care of packaging up our error messages, we can clear this + flag. */ + error_use_protocol = 0; + + protocol = fd_buffer_initialize (protocol_pipe[1], 0, + protocol_memory_error); + + /* At this point we should no longer be using buf_to_net and + buf_from_net. Instead, everything should go through + protocol. */ + buf_to_net = NULL; + buf_from_net = NULL; + + /* These were originally set up to use outbuf_memory_error. + Since we're now in the child, we should use the simpler + protocol_memory_error function. */ + saved_output->memory_error = protocol_memory_error; + saved_outerr->memory_error = protocol_memory_error; + + if (dup2 (dev_null_fd, STDIN_FILENO) < 0) + error (1, errno, "can't set up pipes"); + if (dup2 (stdout_pipe[1], STDOUT_FILENO) < 0) + error (1, errno, "can't set up pipes"); + if (dup2 (stderr_pipe[1], STDERR_FILENO) < 0) + error (1, errno, "can't set up pipes"); + close (stdout_pipe[0]); + close (stderr_pipe[0]); + close (protocol_pipe[0]); +#ifdef SERVER_FLOWCONTROL + close (flowcontrol_pipe[1]); +#endif /* SERVER_FLOWCONTROL */ + + /* + * Set this in .bashrc if you want to give yourself time to attach + * to the subprocess with a debugger. + */ + if (getenv ("CVS_SERVER_SLEEP")) + { + int secs = atoi (getenv ("CVS_SERVER_SLEEP")); + sleep (secs); + } + + exitstatus = (*command) (argument_count, argument_vector); + + /* Output any partial lines. If the client doesn't support + "MT", we just throw out the partial line, like old versions + of CVS did, since the protocol can't support this. */ + if (supported_response ("MT") && ! buf_empty_p (saved_output)) + { + buf_output0 (protocol, "MT text "); + buf_append_buffer (protocol, saved_output); + buf_output (protocol, "\n", 1); + buf_send_counted (protocol); + } + /* For now we just discard partial lines on stderr. I suspect + that CVS can't write such lines unless there is a bug. */ + + /* + * When we exit, that will close the pipes, giving an EOF to + * the parent. + */ + exit (exitstatus); + } + + /* OK, sit around getting all the input from the child. */ + { + struct buffer *stdoutbuf; + struct buffer *stderrbuf; + struct buffer *protocol_inbuf; + /* Number of file descriptors to check in select (). */ + int num_to_check; + int count_needed = 0; +#ifdef SERVER_FLOWCONTROL + int have_flowcontrolled = 0; +#endif /* SERVER_FLOWCONTROL */ + + FD_ZERO (&command_fds_to_drain.fds); + num_to_check = stdout_pipe[0]; + FD_SET (stdout_pipe[0], &command_fds_to_drain.fds); + if (stderr_pipe[0] > num_to_check) + num_to_check = stderr_pipe[0]; + FD_SET (stderr_pipe[0], &command_fds_to_drain.fds); + if (protocol_pipe[0] > num_to_check) + num_to_check = protocol_pipe[0]; + FD_SET (protocol_pipe[0], &command_fds_to_drain.fds); + if (STDOUT_FILENO > num_to_check) + num_to_check = STDOUT_FILENO; + max_command_fd = num_to_check; + /* + * File descriptors are numbered from 0, so num_to_check needs to + * be one larger than the largest descriptor. + */ + ++num_to_check; + if (num_to_check > FD_SETSIZE) + { + buf_output0 (buf_to_net, + "E internal error: FD_SETSIZE not big enough.\n\ +error \n"); + goto error_exit; + } + + stdoutbuf = fd_buffer_initialize (stdout_pipe[0], 1, + input_memory_error); + + stderrbuf = fd_buffer_initialize (stderr_pipe[0], 1, + input_memory_error); + + protocol_inbuf = fd_buffer_initialize (protocol_pipe[0], 1, + input_memory_error); + + set_nonblock (buf_to_net); + set_nonblock (stdoutbuf); + set_nonblock (stderrbuf); + set_nonblock (protocol_inbuf); + + if (close (stdout_pipe[1]) < 0) + { + print_error (errno); + goto error_exit; + } + stdout_pipe[1] = -1; + + if (close (stderr_pipe[1]) < 0) + { + print_error (errno); + goto error_exit; + } + stderr_pipe[1] = -1; + + if (close (protocol_pipe[1]) < 0) + { + print_error (errno); + goto error_exit; + } + protocol_pipe[1] = -1; + +#ifdef SERVER_FLOWCONTROL + if (close (flowcontrol_pipe[0]) < 0) + { + print_error (errno); + goto error_exit; + } + flowcontrol_pipe[0] = -1; +#endif /* SERVER_FLOWCONTROL */ + + if (close (dev_null_fd) < 0) + { + print_error (errno); + goto error_exit; + } + dev_null_fd = -1; + + while (stdout_pipe[0] >= 0 + || stderr_pipe[0] >= 0 + || protocol_pipe[0] >= 0) + { + fd_set readfds; + fd_set writefds; + int numfds; +#ifdef SERVER_FLOWCONTROL + int bufmemsize; + + /* + * See if we are swamping the remote client and filling our VM. + * Tell child to hold off if we do. + */ + bufmemsize = buf_count_mem (buf_to_net); + if (!have_flowcontrolled && (bufmemsize > SERVER_HI_WATER)) + { + if (write(flowcontrol_pipe[1], "S", 1) == 1) + have_flowcontrolled = 1; + } + else if (have_flowcontrolled && (bufmemsize < SERVER_LO_WATER)) + { + if (write(flowcontrol_pipe[1], "G", 1) == 1) + have_flowcontrolled = 0; + } +#endif /* SERVER_FLOWCONTROL */ + + FD_ZERO (&readfds); + FD_ZERO (&writefds); + if (! buf_empty_p (buf_to_net)) + FD_SET (STDOUT_FILENO, &writefds); + + if (stdout_pipe[0] >= 0) + { + FD_SET (stdout_pipe[0], &readfds); + } + if (stderr_pipe[0] >= 0) + { + FD_SET (stderr_pipe[0], &readfds); + } + if (protocol_pipe[0] >= 0) + { + FD_SET (protocol_pipe[0], &readfds); + } + + /* This process of selecting on the three pipes means that + we might not get output in the same order in which it + was written, thus producing the well-known + "out-of-order" bug. If the child process uses + cvs_output and cvs_outerr, it will send everything on + the protocol_pipe and avoid this problem, so the + solution is to use cvs_output and cvs_outerr in the + child process. */ + do { + /* This used to select on exceptions too, but as far + as I know there was never any reason to do that and + SCO doesn't let you select on exceptions on pipes. */ + numfds = select (num_to_check, &readfds, &writefds, + (fd_set *)0, (struct timeval *)NULL); + if (numfds < 0 + && errno != EINTR) + { + print_error (errno); + goto error_exit; + } + } while (numfds < 0); + + if (FD_ISSET (STDOUT_FILENO, &writefds)) + { + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + + if (stdout_pipe[0] >= 0 + && (FD_ISSET (stdout_pipe[0], &readfds))) + { + int status; + + status = buf_input_data (stdoutbuf, (int *) NULL); + + buf_copy_lines (buf_to_net, stdoutbuf, 'M'); + + if (status == -1) + stdout_pipe[0] = -1; + else if (status > 0) + { + print_error (status); + goto error_exit; + } + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + + if (stderr_pipe[0] >= 0 + && (FD_ISSET (stderr_pipe[0], &readfds))) + { + int status; + + status = buf_input_data (stderrbuf, (int *) NULL); + + buf_copy_lines (buf_to_net, stderrbuf, 'E'); + + if (status == -1) + stderr_pipe[0] = -1; + else if (status > 0) + { + print_error (status); + goto error_exit; + } + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + } + + if (protocol_pipe[0] >= 0 + && (FD_ISSET (protocol_pipe[0], &readfds))) + { + int status; + int count_read; + int special; + + status = buf_input_data (protocol_inbuf, &count_read); + + if (status == -1) + protocol_pipe[0] = -1; + else if (status > 0) + { + print_error (status); + goto error_exit; + } + + /* + * We only call buf_copy_counted if we have read + * enough bytes to make it worthwhile. This saves us + * from continually recounting the amount of data we + * have. + */ + count_needed -= count_read; + while (count_needed <= 0) + { + count_needed = buf_copy_counted (buf_to_net, + protocol_inbuf, + &special); + + /* What should we do with errors? syslog() them? */ + buf_send_output (buf_to_net); + + /* If SPECIAL got set to -1, it means that the child + wants us to flush the pipe. We don't want to block + on the network, but we flush what we can. If the + client supports the 'F' command, we send it. */ + if (special == -1) + { + if (supported_response ("F")) + { + buf_append_char (buf_to_net, 'F'); + buf_append_char (buf_to_net, '\n'); + } + + cvs_flusherr (); + } + } + } + } + + /* + * OK, we've gotten EOF on all the pipes. If there is + * anything left on stdoutbuf or stderrbuf (this could only + * happen if there was no trailing newline), send it over. + */ + if (! buf_empty_p (stdoutbuf)) + { + buf_append_char (stdoutbuf, '\n'); + buf_copy_lines (buf_to_net, stdoutbuf, 'M'); + } + if (! buf_empty_p (stderrbuf)) + { + buf_append_char (stderrbuf, '\n'); + buf_copy_lines (buf_to_net, stderrbuf, 'E'); + } + if (! buf_empty_p (protocol_inbuf)) + buf_output0 (buf_to_net, + "E Protocol error: uncounted data discarded\n"); + + errs = 0; + + while (command_pid > 0) + { + int status; + pid_t waited_pid; + waited_pid = waitpid (command_pid, &status, 0); + if (waited_pid < 0) + { + /* + * Intentionally ignoring EINTR. Other errors + * "can't happen". + */ + continue; + } + + if (WIFEXITED (status)) + errs += WEXITSTATUS (status); + else + { + int sig = WTERMSIG (status); + char buf[50]; + /* + * This is really evil, because signals might be numbered + * differently on the two systems. We should be using + * signal names (either of the "Terminated" or the "SIGTERM" + * variety). But cvs doesn't currently use libiberty...we + * could roll our own.... FIXME. + */ + buf_output0 (buf_to_net, "E Terminated with fatal signal "); + sprintf (buf, "%d\n", sig); + buf_output0 (buf_to_net, buf); + + /* Test for a core dump. Is this portable? */ + if (status & 0x80) + { + buf_output0 (buf_to_net, "E Core dumped; preserving "); + buf_output0 (buf_to_net, orig_server_temp_dir); + buf_output0 (buf_to_net, " on server.\n\ +E CVS locks may need cleaning up.\n"); + dont_delete_temp = 1; + } + ++errs; + } + if (waited_pid == command_pid) + command_pid = -1; + } + + /* + * OK, we've waited for the child. By now all CVS locks are free + * and it's OK to block on the network. + */ + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + } + + if (errs) + /* We will have printed an error message already. */ + buf_output0 (buf_to_net, "error \n"); + else + buf_output0 (buf_to_net, "ok\n"); + goto free_args_and_return; + + error_exit: + if (command_pid > 0) + kill (command_pid, SIGTERM); + + while (command_pid > 0) + { + pid_t waited_pid; + waited_pid = waitpid (command_pid, (int *) 0, 0); + if (waited_pid < 0 && errno == EINTR) + continue; + if (waited_pid == command_pid) + command_pid = -1; + } + + close (dev_null_fd); + close (protocol_pipe[0]); + close (protocol_pipe[1]); + close (stderr_pipe[0]); + close (stderr_pipe[1]); + close (stdout_pipe[0]); + close (stdout_pipe[1]); + + free_args_and_return: + /* Now free the arguments. */ + { + /* argument_vector[0] is a dummy argument, we don't mess with it. */ + char **cp; + for (cp = argument_vector + 1; + cp < argument_vector + argument_count; + ++cp) + free (*cp); + + argument_count = 1; + } + + /* Flush out any data not yet sent. */ + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + + return; +} + +#ifdef SERVER_FLOWCONTROL +/* + * Called by the child at convenient points in the server's execution for + * the server child to block.. ie: when it has no locks active. + */ +void +server_pause_check() +{ + int paused = 0; + char buf[1]; + + while (read (flowcontrol_pipe[0], buf, 1) == 1) + { + if (*buf == 'S') /* Stop */ + paused = 1; + else if (*buf == 'G') /* Go */ + paused = 0; + else + return; /* ??? */ + } + while (paused) { + int numfds, numtocheck; + fd_set fds; + + FD_ZERO (&fds); + FD_SET (flowcontrol_pipe[0], &fds); + numtocheck = flowcontrol_pipe[0] + 1; + + do { + numfds = select (numtocheck, &fds, (fd_set *)0, + (fd_set *)0, (struct timeval *)NULL); + if (numfds < 0 + && errno != EINTR) + { + print_error (errno); + return; + } + } while (numfds < 0); + + if (FD_ISSET (flowcontrol_pipe[0], &fds)) + { + int got; + + while ((got = read (flowcontrol_pipe[0], buf, 1)) == 1) + { + if (*buf == 'S') /* Stop */ + paused = 1; + else if (*buf == 'G') /* Go */ + paused = 0; + else + return; /* ??? */ + } + + /* This assumes that we are using BSD or POSIX nonblocking + I/O. System V nonblocking I/O returns zero if there is + nothing to read. */ + if (got == 0) + error (1, 0, "flow control EOF"); + if (got < 0 && ! blocking_error (errno)) + { + error (1, errno, "flow control read failed"); + } + } + } +} +#endif /* SERVER_FLOWCONTROL */ + +/* This variable commented in server.h. */ +char *server_dir = NULL; + +static void output_dir PROTO((char *, char *)); + +static void +output_dir (update_dir, repository) + char *update_dir; + char *repository; +{ + if (server_dir != NULL) + { + buf_output0 (protocol, server_dir); + buf_output0 (protocol, "/"); + } + if (update_dir[0] == '\0') + buf_output0 (protocol, "."); + else + buf_output0 (protocol, update_dir); + buf_output0 (protocol, "/\n"); + buf_output0 (protocol, repository); + buf_output0 (protocol, "/"); +} + +/* + * Entries line that we are squirreling away to send to the client when + * we are ready. + */ +static char *entries_line; + +/* + * File which has been Scratch_File'd, we are squirreling away that fact + * to inform the client when we are ready. + */ +static char *scratched_file; + +/* + * The scratched_file will need to be removed as well as having its entry + * removed. + */ +static int kill_scratched_file; + +void +server_register (name, version, timestamp, options, tag, date, conflict) + char *name; + char *version; + char *timestamp; + char *options; + char *tag; + char *date; + char *conflict; +{ + int len; + + if (options == NULL) + options = ""; + + if (trace) + { + (void) fprintf (stderr, + "%s-> server_register(%s, %s, %s, %s, %s, %s, %s)\n", + CLIENT_SERVER_STR, + name, version, timestamp ? timestamp : "", options, + tag ? tag : "", date ? date : "", + conflict ? conflict : ""); + } + + if (entries_line != NULL) + { + /* + * If CVS decides to Register it more than once (which happens + * on "cvs update foo/foo.c" where foo and foo.c are already + * checked out), use the last of the entries lines Register'd. + */ + free (entries_line); + } + + /* + * I have reports of Scratch_Entry and Register both happening, in + * two different cases. Using the last one which happens is almost + * surely correct; I haven't tracked down why they both happen (or + * even verified that they are for the same file). + */ + if (scratched_file != NULL) + { + free (scratched_file); + scratched_file = NULL; + } + + len = (strlen (name) + strlen (version) + strlen (options) + 80); + if (tag) + len += strlen (tag); + if (date) + len += strlen (date); + + entries_line = xmalloc (len); + sprintf (entries_line, "/%s/%s/", name, version); + if (conflict != NULL) + { + strcat (entries_line, "+="); + } + strcat (entries_line, "/"); + strcat (entries_line, options); + strcat (entries_line, "/"); + if (tag != NULL) + { + strcat (entries_line, "T"); + strcat (entries_line, tag); + } + else if (date != NULL) + { + strcat (entries_line, "D"); + strcat (entries_line, date); + } +} + +void +server_scratch (fname) + char *fname; +{ + /* + * I have reports of Scratch_Entry and Register both happening, in + * two different cases. Using the last one which happens is almost + * surely correct; I haven't tracked down why they both happen (or + * even verified that they are for the same file). + */ + if (entries_line != NULL) + { + free (entries_line); + entries_line = NULL; + } + + if (scratched_file != NULL) + { + buf_output0 (protocol, + "E CVS server internal error: duplicate Scratch_Entry\n"); + buf_send_counted (protocol); + return; + } + scratched_file = xstrdup (fname); + kill_scratched_file = 1; +} + +void +server_scratch_entry_only () +{ + kill_scratched_file = 0; +} + +/* Print a new entries line, from a previous server_register. */ +static void +new_entries_line () +{ + if (entries_line) + { + buf_output0 (protocol, entries_line); + buf_output (protocol, "\n", 1); + } + else + /* Return the error message as the Entries line. */ + buf_output0 (protocol, + "CVS server internal error: Register missing\n"); + free (entries_line); + entries_line = NULL; +} + + +static void +serve_ci (arg) + char *arg; +{ + do_cvs_command ("commit", commit); +} + +static void +checked_in_response (file, update_dir, repository) + char *file; + char *update_dir; + char *repository; +{ + if (supported_response ("Mode")) + { + struct stat sb; + char *mode_string; + + if ( CVS_STAT (file, &sb) < 0) + { + /* Not clear to me why the file would fail to exist, but it + was happening somewhere in the testsuite. */ + if (!existence_error (errno)) + error (0, errno, "cannot stat %s", file); + } + else + { + buf_output0 (protocol, "Mode "); + mode_string = mode_to_string (sb.st_mode); + buf_output0 (protocol, mode_string); + buf_output0 (protocol, "\n"); + free (mode_string); + } + } + + buf_output0 (protocol, "Checked-in "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + new_entries_line (); +} + +void +server_checked_in (file, update_dir, repository) + char *file; + char *update_dir; + char *repository; +{ + if (noexec) + return; + if (scratched_file != NULL && entries_line == NULL) + { + /* + * This happens if we are now doing a "cvs remove" after a previous + * "cvs add" (without a "cvs ci" in between). + */ + buf_output0 (protocol, "Remove-entry "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + free (scratched_file); + scratched_file = NULL; + } + else + { + checked_in_response (file, update_dir, repository); + } + buf_send_counted (protocol); +} + +void +server_update_entries (file, update_dir, repository, updated) + char *file; + char *update_dir; + char *repository; + enum server_updated_arg4 updated; +{ + if (noexec) + return; + if (updated == SERVER_UPDATED) + checked_in_response (file, update_dir, repository); + else + { + if (!supported_response ("New-entry")) + return; + buf_output0 (protocol, "New-entry "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output (protocol, "\n", 1); + new_entries_line (); + } + + buf_send_counted (protocol); +} + +static void +serve_update (arg) + char *arg; +{ + do_cvs_command ("update", update); +} + +static void +serve_diff (arg) + char *arg; +{ + do_cvs_command ("diff", diff); +} + +static void +serve_log (arg) + char *arg; +{ + do_cvs_command ("log", cvslog); +} + +static void +serve_add (arg) + char *arg; +{ + do_cvs_command ("add", add); +} + +static void +serve_remove (arg) + char *arg; +{ + do_cvs_command ("remove", cvsremove); +} + +static void +serve_status (arg) + char *arg; +{ + do_cvs_command ("status", cvsstatus); +} + +static void +serve_rdiff (arg) + char *arg; +{ + do_cvs_command ("rdiff", patch); +} + +static void +serve_tag (arg) + char *arg; +{ + do_cvs_command ("cvstag", cvstag); +} + +static void +serve_rtag (arg) + char *arg; +{ + do_cvs_command ("rtag", rtag); +} + +static void +serve_import (arg) + char *arg; +{ + do_cvs_command ("import", import); +} + +static void +serve_admin (arg) + char *arg; +{ + do_cvs_command ("admin", admin); +} + +static void +serve_history (arg) + char *arg; +{ + do_cvs_command ("history", history); +} + +static void +serve_release (arg) + char *arg; +{ + do_cvs_command ("release", release); +} + +static void serve_watch_on PROTO ((char *)); + +static void +serve_watch_on (arg) + char *arg; +{ + do_cvs_command ("watch_on", watch_on); +} + +static void serve_watch_off PROTO ((char *)); + +static void +serve_watch_off (arg) + char *arg; +{ + do_cvs_command ("watch_off", watch_off); +} + +static void serve_watch_add PROTO ((char *)); + +static void +serve_watch_add (arg) + char *arg; +{ + do_cvs_command ("watch_add", watch_add); +} + +static void serve_watch_remove PROTO ((char *)); + +static void +serve_watch_remove (arg) + char *arg; +{ + do_cvs_command ("watch_remove", watch_remove); +} + +static void serve_watchers PROTO ((char *)); + +static void +serve_watchers (arg) + char *arg; +{ + do_cvs_command ("watchers", watchers); +} + +static void serve_editors PROTO ((char *)); + +static void +serve_editors (arg) + char *arg; +{ + do_cvs_command ("editors", editors); +} + +static int noop PROTO ((int, char **)); + +static int +noop (argc, argv) + int argc; + char **argv; +{ + return 0; +} + +static void serve_noop PROTO ((char *)); + +static void +serve_noop (arg) + char *arg; +{ + do_cvs_command ("noop", noop); +} + +static void serve_init PROTO ((char *)); + +static void +serve_init (arg) + char *arg; +{ + if (!isabsolute (arg)) + { + if (alloc_pending (80 + strlen (arg))) + sprintf (pending_error_text, + "E Root %s must be an absolute pathname", arg); + /* Fall through to do_cvs_command which will return the + actual error. */ + } + set_local_cvsroot (arg); + + do_cvs_command ("init", init); +} + +static void serve_annotate PROTO ((char *)); + +static void +serve_annotate (arg) + char *arg; +{ + do_cvs_command ("annotate", annotate); +} + +static void +serve_co (arg) + char *arg; +{ + char *tempdir; + int status; + + if (print_pending_error ()) + return; + + if (!isdir (CVSADM)) + { + /* + * The client has not sent a "Repository" line. Check out + * into a pristine directory. + */ + tempdir = malloc (strlen (server_temp_dir) + 80); + if (tempdir == NULL) + { + buf_output0 (buf_to_net, "E Out of memory\n"); + return; + } + strcpy (tempdir, server_temp_dir); + strcat (tempdir, "/checkout-dir"); + status = mkdir_p (tempdir); + if (status != 0 && status != EEXIST) + { + buf_output0 (buf_to_net, "E Cannot create "); + buf_output0 (buf_to_net, tempdir); + buf_append_char (buf_to_net, '\n'); + print_error (errno); + free (tempdir); + return; + } + + if ( CVS_CHDIR (tempdir) < 0) + { + buf_output0 (buf_to_net, "E Cannot change to directory "); + buf_output0 (buf_to_net, tempdir); + buf_append_char (buf_to_net, '\n'); + print_error (errno); + free (tempdir); + return; + } + free (tempdir); + } + + /* Compensate for server_export()'s setting of command_name. + * + * [It probably doesn't matter if do_cvs_command() gets "export" + * or "checkout", but we ought to be accurate where possible.] + */ + do_cvs_command ((strcmp (command_name, "export") == 0) ? + "export" : "checkout", + checkout); +} + +static void +serve_export (arg) + char *arg; +{ + /* Tell checkout() to behave like export not checkout. */ + command_name = "export"; + serve_co (arg); +} + +void +server_copy_file (file, update_dir, repository, newfile) + char *file; + char *update_dir; + char *repository; + char *newfile; +{ + /* At least for now, our practice is to have the server enforce + noexec for the repository and the client enforce it for the + working directory. This might want more thought, and/or + documentation in cvsclient.texi (other responses do it + differently). */ + + if (!supported_response ("Copy-file")) + return; + buf_output0 (protocol, "Copy-file "); + output_dir (update_dir, repository); + buf_output0 (protocol, file); + buf_output0 (protocol, "\n"); + buf_output0 (protocol, newfile); + buf_output0 (protocol, "\n"); +} + +/* See server.h for description. */ + +void +server_modtime (finfo, vers_ts) + struct file_info *finfo; + Vers_TS *vers_ts; +{ + char date[MAXDATELEN]; + char outdate[MAXDATELEN]; + + assert (vers_ts->vn_rcs != NULL); + + if (!supported_response ("Mod-time")) + return; + + if (RCS_getrevtime (finfo->rcs, vers_ts->vn_rcs, date, 0) == (time_t) -1) + /* FIXME? should we be printing some kind of warning? For one + thing I'm not 100% sure whether this happens in non-error + circumstances. */ + return; + date_to_internet (outdate, date); + buf_output0 (protocol, "Mod-time "); + buf_output0 (protocol, outdate); + buf_output0 (protocol, "\n"); +} + +/* See server.h for description. */ + +#if defined (USE_PROTOTYPES) ? USE_PROTOTYPES : defined (__STDC__) +/* Need to prototype because mode_t might be smaller than int. */ +void +server_updated ( + struct file_info *finfo, + Vers_TS *vers, + enum server_updated_arg4 updated, + mode_t mode, + unsigned char *checksum, + struct buffer *filebuf) +#else +void +server_updated (finfo, vers, updated, mode, checksum, filebuf) + struct file_info *finfo; + Vers_TS *vers; + enum server_updated_arg4 updated; + mode_t mode; + unsigned char *checksum; + struct buffer *filebuf; +#endif +{ + if (noexec) + { + /* Hmm, maybe if we did the same thing for entries_file, we + could get rid of the kludges in server_register and + server_scratch which refrain from warning if both + Scratch_Entry and Register get called. Maybe. */ + if (scratched_file) + { + free (scratched_file); + scratched_file = NULL; + } + return; + } + + if (entries_line != NULL && scratched_file == NULL) + { + FILE *f; + struct buffer_data *list, *last; + unsigned long size; + char size_text[80]; + + /* The contents of the file will be in one of filebuf, + list/last, or here. */ + unsigned char *file; + size_t file_allocated; + size_t file_used; + + if (filebuf != NULL) + { + size = buf_length (filebuf); + if (mode == (mode_t) -1) + error (1, 0, "\ +CVS server internal error: no mode in server_updated"); + } + else + { + struct stat sb; + + if ( CVS_STAT (finfo->file, &sb) < 0) + { + if (existence_error (errno)) + { + /* If we have a sticky tag for a branch on which + the file is dead, and cvs update the directory, + it gets a T_CHECKOUT but no file. So in this + case just forget the whole thing. */ + free (entries_line); + entries_line = NULL; + goto done; + } + error (1, errno, "reading %s", finfo->fullname); + } + size = sb.st_size; + if (mode == (mode_t) -1) + { + /* FIXME: When we check out files the umask of the + server (set in .bashrc if rsh is in use) affects + what mode we send, and it shouldn't. */ + mode = sb.st_mode; + } + } + + if (checksum != NULL) + { + static int checksum_supported = -1; + + if (checksum_supported == -1) + { + checksum_supported = supported_response ("Checksum"); + } + + if (checksum_supported) + { + int i; + char buf[3]; + + buf_output0 (protocol, "Checksum "); + for (i = 0; i < 16; i++) + { + sprintf (buf, "%02x", (unsigned int) checksum[i]); + buf_output0 (protocol, buf); + } + buf_append_char (protocol, '\n'); + } + } + + if (updated == SERVER_UPDATED) + { + Node *node; + Entnode *entnode; + + if (!(supported_response ("Created") + && supported_response ("Update-existing"))) + buf_output0 (protocol, "Updated "); + else + { + assert (vers != NULL); + if (vers->ts_user == NULL) + buf_output0 (protocol, "Created "); + else + buf_output0 (protocol, "Update-existing "); + } + + /* Now munge the entries to say that the file is unmodified, + in case we end up processing it again (e.g. modules3-6 + in the testsuite). */ + node = findnode_fn (finfo->entries, finfo->file); + entnode = (Entnode *)node->data; + free (entnode->timestamp); + entnode->timestamp = xstrdup ("="); + } + else if (updated == SERVER_MERGED) + buf_output0 (protocol, "Merged "); + else if (updated == SERVER_PATCHED) + buf_output0 (protocol, "Patched "); + else if (updated == SERVER_RCS_DIFF) + buf_output0 (protocol, "Rcs-diff "); + else + abort (); + output_dir (finfo->update_dir, finfo->repository); + buf_output0 (protocol, finfo->file); + buf_output (protocol, "\n", 1); + + new_entries_line (); + + { + char *mode_string; + + mode_string = mode_to_string (mode); + buf_output0 (protocol, mode_string); + buf_output0 (protocol, "\n"); + free (mode_string); + } + + list = last = NULL; + + file = NULL; + file_allocated = 0; + file_used = 0; + + if (size > 0) + { + /* Throughout this section we use binary mode to read the + file we are sending. The client handles any line ending + translation if necessary. */ + + if (file_gzip_level + /* + * For really tiny files, the gzip process startup + * time will outweigh the compression savings. This + * might be computable somehow; using 100 here is just + * a first approximation. + */ + && size > 100) + { + /* Basing this routine on read_and_gzip is not a + high-performance approach. But it seems easier + to code than the alternative (and less + vulnerable to subtle bugs). Given that this feature + is mainly for compatibility, that is the better + tradeoff. */ + + int fd; + + /* Callers must avoid passing us a buffer if + file_gzip_level is set. We could handle this case, + but it's not worth it since this case never arises + with a current client and server. */ + if (filebuf != NULL) + error (1, 0, "\ +CVS server internal error: unhandled case in server_updated"); + + fd = CVS_OPEN (finfo->file, O_RDONLY | OPEN_BINARY, 0); + if (fd < 0) + error (1, errno, "reading %s", finfo->fullname); + if (read_and_gzip (fd, finfo->fullname, &file, + &file_allocated, &file_used, + file_gzip_level)) + error (1, 0, "aborting due to compression error"); + size = file_used; + if (close (fd) < 0) + error (1, errno, "reading %s", finfo->fullname); + /* Prepending length with "z" is flag for using gzip here. */ + buf_output0 (protocol, "z"); + } + else if (filebuf == NULL) + { + long status; + + f = CVS_FOPEN (finfo->file, "rb"); + if (f == NULL) + error (1, errno, "reading %s", finfo->fullname); + status = buf_read_file (f, size, &list, &last); + if (status == -2) + (*protocol->memory_error) (protocol); + else if (status != 0) + error (1, ferror (f) ? errno : 0, "reading %s", + finfo->fullname); + if (fclose (f) == EOF) + error (1, errno, "reading %s", finfo->fullname); + } + } + + sprintf (size_text, "%lu\n", size); + buf_output0 (protocol, size_text); + + if (file != NULL) + { + buf_output (protocol, file, file_used); + free (file); + file = NULL; + } + else if (filebuf == NULL) + buf_append_data (protocol, list, last); + else + { + buf_append_buffer (protocol, filebuf); + buf_free (filebuf); + } + /* Note we only send a newline here if the file ended with one. */ + + /* + * Avoid using up too much disk space for temporary files. + * A file which does not exist indicates that the file is up-to-date, + * which is now the case. If this is SERVER_MERGED, the file is + * not up-to-date, and we indicate that by leaving the file there. + * I'm thinking of cases like "cvs update foo/foo.c foo". + */ + if ((updated == SERVER_UPDATED + || updated == SERVER_PATCHED + || updated == SERVER_RCS_DIFF) + && filebuf == NULL + /* But if we are joining, we'll need the file when we call + join_file. */ + && !joining ()) + { + if (CVS_UNLINK (finfo->file) < 0) + error (0, errno, "cannot remove temp file for %s", + finfo->fullname); + } + } + else if (scratched_file != NULL && entries_line == NULL) + { + if (strcmp (scratched_file, finfo->file) != 0) + error (1, 0, + "CVS server internal error: `%s' vs. `%s' scratched", + scratched_file, + finfo->file); + free (scratched_file); + scratched_file = NULL; + + if (kill_scratched_file) + buf_output0 (protocol, "Removed "); + else + buf_output0 (protocol, "Remove-entry "); + output_dir (finfo->update_dir, finfo->repository); + buf_output0 (protocol, finfo->file); + buf_output (protocol, "\n", 1); + } + else if (scratched_file == NULL && entries_line == NULL) + { + /* + * This can happen with death support if we were processing + * a dead file in a checkout. + */ + } + else + error (1, 0, + "CVS server internal error: Register *and* Scratch_Entry.\n"); + buf_send_counted (protocol); + done:; +} + +/* Return whether we should send patches in RCS format. */ + +int +server_use_rcs_diff () +{ + return supported_response ("Rcs-diff"); +} + +void +server_set_entstat (update_dir, repository) + char *update_dir; + char *repository; +{ + static int set_static_supported = -1; + if (set_static_supported == -1) + set_static_supported = supported_response ("Set-static-directory"); + if (!set_static_supported) return; + + buf_output0 (protocol, "Set-static-directory "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_send_counted (protocol); +} + +void +server_clear_entstat (update_dir, repository) + char *update_dir; + char *repository; +{ + static int clear_static_supported = -1; + if (clear_static_supported == -1) + clear_static_supported = supported_response ("Clear-static-directory"); + if (!clear_static_supported) return; + + if (noexec) + return; + + buf_output0 (protocol, "Clear-static-directory "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + buf_send_counted (protocol); +} + +void +server_set_sticky (update_dir, repository, tag, date, nonbranch) + char *update_dir; + char *repository; + char *tag; + char *date; + int nonbranch; +{ + static int set_sticky_supported = -1; + + assert (update_dir != NULL); + + if (set_sticky_supported == -1) + set_sticky_supported = supported_response ("Set-sticky"); + if (!set_sticky_supported) return; + + if (noexec) + return; + + if (tag == NULL && date == NULL) + { + buf_output0 (protocol, "Clear-sticky "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + } + else + { + buf_output0 (protocol, "Set-sticky "); + output_dir (update_dir, repository); + buf_output0 (protocol, "\n"); + if (tag != NULL) + { + if (nonbranch) + buf_output0 (protocol, "N"); + else + buf_output0 (protocol, "T"); + buf_output0 (protocol, tag); + } + else + { + buf_output0 (protocol, "D"); + buf_output0 (protocol, date); + } + buf_output0 (protocol, "\n"); + } + buf_send_counted (protocol); +} + +struct template_proc_data +{ + char *update_dir; + char *repository; +}; + +/* Here as a static until we get around to fixing Parse_Info to pass along + a void * for it. */ +static struct template_proc_data *tpd; + +static int +template_proc (repository, template) + char *repository; + char *template; +{ + FILE *fp; + char buf[1024]; + size_t n; + struct stat sb; + struct template_proc_data *data = tpd; + + if (!supported_response ("Template")) + /* Might want to warn the user that the rcsinfo feature won't work. */ + return 0; + buf_output0 (protocol, "Template "); + output_dir (data->update_dir, data->repository); + buf_output0 (protocol, "\n"); + + fp = CVS_FOPEN (template, "rb"); + if (fp == NULL) + { + error (0, errno, "Couldn't open rcsinfo template file %s", template); + return 1; + } + if (fstat (fileno (fp), &sb) < 0) + { + error (0, errno, "cannot stat rcsinfo template file %s", template); + return 1; + } + sprintf (buf, "%ld\n", (long) sb.st_size); + buf_output0 (protocol, buf); + while (!feof (fp)) + { + n = fread (buf, 1, sizeof buf, fp); + buf_output (protocol, buf, n); + if (ferror (fp)) + { + error (0, errno, "cannot read rcsinfo template file %s", template); + (void) fclose (fp); + return 1; + } + } + if (fclose (fp) < 0) + error (0, errno, "cannot close rcsinfo template file %s", template); + return 0; +} + +void +server_template (update_dir, repository) + char *update_dir; + char *repository; +{ + struct template_proc_data data; + data.update_dir = update_dir; + data.repository = repository; + tpd = &data; + (void) Parse_Info (CVSROOTADM_RCSINFO, repository, template_proc, 1); +} + +static void +serve_gzip_contents (arg) + char *arg; +{ + int level; + level = atoi (arg); + if (level == 0) + level = 6; + file_gzip_level = level; +} + +static void +serve_gzip_stream (arg) + char *arg; +{ + int level; + level = atoi (arg); + if (level == 0) + level = 6; + + /* All further communication with the client will be compressed. */ + + buf_to_net = compress_buffer_initialize (buf_to_net, 0, level, + buf_to_net->memory_error); + buf_from_net = compress_buffer_initialize (buf_from_net, 1, level, + buf_from_net->memory_error); +} + +/* Tell the client about RCS options set in CVSROOT/cvswrappers. */ +static void +serve_wrapper_sendme_rcs_options (arg) + char *arg; +{ + /* Actually, this is kind of sdrawkcab-ssa: the client wants + * verbatim lines from a cvswrappers file, but the server has + * already parsed the cvswrappers file into the wrap_list struct. + * Therefore, the server loops over wrap_list, unparsing each + * entry before sending it. + */ + char *wrapper_line = NULL; + + wrap_setup (); + + for (wrap_unparse_rcs_options (&wrapper_line, 1); + wrapper_line; + wrap_unparse_rcs_options (&wrapper_line, 0)) + { + buf_output0 (buf_to_net, "Wrapper-rcsOption "); + buf_output0 (buf_to_net, wrapper_line); + buf_output0 (buf_to_net, "\012");; + free (wrapper_line); + } + + buf_output0 (buf_to_net, "ok\012"); + + /* The client is waiting for us, so we better send the data now. */ + buf_flush (buf_to_net, 1); +} + + +static void +serve_ignore (arg) + char *arg; +{ + /* + * Just ignore this command. This is used to support the + * update-patches command, which is not a real command, but a signal + * to the client that update will accept the -u argument. + */ +} + +static int +expand_proc (pargc, argv, where, mwhere, mfile, shorten, + local_specified, omodule, msg) + int *pargc; + char **argv; + char *where; + char *mwhere; + char *mfile; + int shorten; + int local_specified; + char *omodule; + char *msg; +{ + int i; + char *dir = argv[0]; + + /* If mwhere has been specified, the thing we're expanding is a + module -- just return its name so the client will ask for the + right thing later. If it is an alias or a real directory, + mwhere will not be set, so send out the appropriate + expansion. */ + + if (mwhere != NULL) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, mwhere); + if (mfile != NULL) + { + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, mfile); + } + buf_append_char (buf_to_net, '\n'); + } + else + { + /* We may not need to do this anymore -- check the definition + of aliases before removing */ + if (*pargc == 1) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '\n'); + } + else + { + for (i = 1; i < *pargc; ++i) + { + buf_output0 (buf_to_net, "Module-expansion "); + if (server_dir != NULL) + { + buf_output0 (buf_to_net, server_dir); + buf_output0 (buf_to_net, "/"); + } + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '/'); + buf_output0 (buf_to_net, argv[i]); + buf_append_char (buf_to_net, '\n'); + } + } + } + return 0; +} + +static void +serve_expand_modules (arg) + char *arg; +{ + int i; + int err; + DBM *db; + err = 0; + + server_expanding = 1; + db = open_module (); + for (i = 1; i < argument_count; i++) + err += do_module (db, argument_vector[i], + CHECKOUT, "Updating", expand_proc, + NULL, 0, 0, 0, + (char *) NULL); + close_module (db); + server_expanding = 0; + { + /* argument_vector[0] is a dummy argument, we don't mess with it. */ + char **cp; + for (cp = argument_vector + 1; + cp < argument_vector + argument_count; + ++cp) + free (*cp); + + argument_count = 1; + } + if (err) + /* We will have printed an error message already. */ + buf_output0 (buf_to_net, "error \n"); + else + buf_output0 (buf_to_net, "ok\n"); + + /* The client is waiting for the module expansions, so we must + send the output now. */ + buf_flush (buf_to_net, 1); +} + +void +server_prog (dir, name, which) + char *dir; + char *name; + enum progs which; +{ + if (!supported_response ("Set-checkin-prog")) + { + buf_output0 (buf_to_net, "E \ +warning: this client does not support -i or -u flags in the modules file.\n"); + return; + } + switch (which) + { + case PROG_CHECKIN: + buf_output0 (buf_to_net, "Set-checkin-prog "); + break; + case PROG_UPDATE: + buf_output0 (buf_to_net, "Set-update-prog "); + break; + } + buf_output0 (buf_to_net, dir); + buf_append_char (buf_to_net, '\n'); + buf_output0 (buf_to_net, name); + buf_append_char (buf_to_net, '\n'); +} + +static void +serve_checkin_prog (arg) + char *arg; +{ + FILE *f; + f = CVS_FOPEN (CVSADM_CIPROG, "w+"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_CIPROG))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_CIPROG); + return; + } + if (fprintf (f, "%s\n", arg) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_CIPROG))) + sprintf (pending_error_text, + "E cannot write to %s", CVSADM_CIPROG); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_CIPROG))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_CIPROG); + return; + } +} + +static void +serve_update_prog (arg) + char *arg; +{ + FILE *f; + + /* Before we do anything we need to make sure we are not in readonly + mode. */ + if (!check_command_legal_p ("commit")) + { + /* I might be willing to make this a warning, except we lack the + machinery to do so. */ + if (alloc_pending (80)) + sprintf (pending_error_text, "\ +E Flag -u in modules not allowed in readonly mode"); + return; + } + + f = CVS_FOPEN (CVSADM_UPROG, "w+"); + if (f == NULL) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_UPROG))) + sprintf (pending_error_text, "E cannot open %s", CVSADM_UPROG); + return; + } + if (fprintf (f, "%s\n", arg) < 0) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_UPROG))) + sprintf (pending_error_text, "E cannot write to %s", CVSADM_UPROG); + return; + } + if (fclose (f) == EOF) + { + pending_error = errno; + if (alloc_pending (80 + strlen (CVSADM_UPROG))) + sprintf (pending_error_text, "E cannot close %s", CVSADM_UPROG); + return; + } +} + +static void serve_valid_requests PROTO((char *arg)); + +#endif /* SERVER_SUPPORT */ +#if defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT) + +/* + * Parts of this table are shared with the client code, + * but the client doesn't need to know about the handler + * functions. + */ + +struct request requests[] = +{ +#ifdef SERVER_SUPPORT +#define REQ_LINE(n, f, s) {n, f, s} +#else +#define REQ_LINE(n, f, s) {n, s} +#endif + + REQ_LINE("Root", serve_root, RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("Valid-responses", serve_valid_responses, + RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("valid-requests", serve_valid_requests, + RQ_ESSENTIAL | RQ_ROOTLESS), + REQ_LINE("Repository", serve_repository, 0), + REQ_LINE("Directory", serve_directory, RQ_ESSENTIAL), + REQ_LINE("Max-dotdot", serve_max_dotdot, 0), + REQ_LINE("Static-directory", serve_static_directory, 0), + REQ_LINE("Sticky", serve_sticky, 0), + REQ_LINE("Checkin-prog", serve_checkin_prog, 0), + REQ_LINE("Update-prog", serve_update_prog, 0), + REQ_LINE("Entry", serve_entry, RQ_ESSENTIAL), + REQ_LINE("Kopt", serve_kopt, 0), + REQ_LINE("Checkin-time", serve_checkin_time, 0), + REQ_LINE("Modified", serve_modified, RQ_ESSENTIAL), + REQ_LINE("Is-modified", serve_is_modified, 0), + + /* The client must send this request to interoperate with CVS 1.5 + through 1.9 servers. The server must support it (although it can + be and is a noop) to interoperate with CVS 1.5 to 1.9 clients. */ + REQ_LINE("UseUnchanged", serve_enable_unchanged, RQ_ENABLEME | RQ_ROOTLESS), + + REQ_LINE("Unchanged", serve_unchanged, RQ_ESSENTIAL), + REQ_LINE("Notify", serve_notify, 0), + REQ_LINE("Questionable", serve_questionable, 0), + REQ_LINE("Case", serve_case, 0), + REQ_LINE("Argument", serve_argument, RQ_ESSENTIAL), + REQ_LINE("Argumentx", serve_argumentx, RQ_ESSENTIAL), + REQ_LINE("Global_option", serve_global_option, 0), + REQ_LINE("Gzip-stream", serve_gzip_stream, 0), + REQ_LINE("wrapper-sendme-rcsOptions", + serve_wrapper_sendme_rcs_options, + 0), + REQ_LINE("Set", serve_set, RQ_ROOTLESS), +#ifdef ENCRYPTION +# ifdef HAVE_KERBEROS + REQ_LINE("Kerberos-encrypt", serve_kerberos_encrypt, 0), +# endif +# ifdef HAVE_GSSAPI + REQ_LINE("Gssapi-encrypt", serve_gssapi_encrypt, 0), +# endif +#endif +#ifdef HAVE_GSSAPI + REQ_LINE("Gssapi-authenticate", serve_gssapi_authenticate, 0), +#endif + REQ_LINE("expand-modules", serve_expand_modules, 0), + REQ_LINE("ci", serve_ci, RQ_ESSENTIAL), + REQ_LINE("co", serve_co, RQ_ESSENTIAL), + REQ_LINE("update", serve_update, RQ_ESSENTIAL), + REQ_LINE("diff", serve_diff, 0), + REQ_LINE("log", serve_log, 0), + REQ_LINE("add", serve_add, 0), + REQ_LINE("remove", serve_remove, 0), + REQ_LINE("update-patches", serve_ignore, 0), + REQ_LINE("gzip-file-contents", serve_gzip_contents, 0), + REQ_LINE("status", serve_status, 0), + REQ_LINE("rdiff", serve_rdiff, 0), + REQ_LINE("tag", serve_tag, 0), + REQ_LINE("rtag", serve_rtag, 0), + REQ_LINE("import", serve_import, 0), + REQ_LINE("admin", serve_admin, 0), + REQ_LINE("export", serve_export, 0), + REQ_LINE("history", serve_history, 0), + REQ_LINE("release", serve_release, 0), + REQ_LINE("watch-on", serve_watch_on, 0), + REQ_LINE("watch-off", serve_watch_off, 0), + REQ_LINE("watch-add", serve_watch_add, 0), + REQ_LINE("watch-remove", serve_watch_remove, 0), + REQ_LINE("watchers", serve_watchers, 0), + REQ_LINE("editors", serve_editors, 0), + REQ_LINE("init", serve_init, RQ_ROOTLESS), + REQ_LINE("annotate", serve_annotate, 0), + REQ_LINE("noop", serve_noop, 0), + REQ_LINE(NULL, NULL, 0) + +#undef REQ_LINE +}; + +#endif /* SERVER_SUPPORT or CLIENT_SUPPORT */ +#ifdef SERVER_SUPPORT + +static void +serve_valid_requests (arg) + char *arg; +{ + struct request *rq; + if (print_pending_error ()) + return; + buf_output0 (buf_to_net, "Valid-requests"); + for (rq = requests; rq->name != NULL; rq++) + { + if (rq->func != NULL) + { + buf_append_char (buf_to_net, ' '); + buf_output0 (buf_to_net, rq->name); + } + } + buf_output0 (buf_to_net, "\nok\n"); + + /* The client is waiting for the list of valid requests, so we + must send the output now. */ + buf_flush (buf_to_net, 1); +} + +#ifdef sun +/* + * Delete temporary files. SIG is the signal making this happen, or + * 0 if not called as a result of a signal. + */ +static int command_pid_is_dead; +static void wait_sig (sig) + int sig; +{ + int status; + pid_t r = wait (&status); + if (r == command_pid) + command_pid_is_dead++; +} +#endif + +void +server_cleanup (sig) + int sig; +{ + /* Do "rm -rf" on the temp directory. */ + int status; + int save_noexec; + + if (buf_to_net != NULL) + { + /* FIXME: If this is not the final call from server, this + could deadlock, because the client might be blocked writing + to us. This should not be a problem in practice, because + we do not generate much output when the client is not + waiting for it. */ + set_block (buf_to_net); + buf_flush (buf_to_net, 1); + + /* The calls to buf_shutdown are currently only meaningful + when we are using compression. First we shut down + BUF_FROM_NET. That will pick up the checksum generated + when the client shuts down its buffer. Then, after we have + generated any final output, we shut down BUF_TO_NET. */ + + status = buf_shutdown (buf_from_net); + if (status != 0) + { + error (0, status, "shutting down buffer from client"); + buf_flush (buf_to_net, 1); + } + } + + if (dont_delete_temp) + { + if (buf_to_net != NULL) + (void) buf_shutdown (buf_to_net); + return; + } + + /* What a bogus kludge. This disgusting code makes all kinds of + assumptions about SunOS, and is only for a bug in that system. + So only enable it on Suns. */ +#ifdef sun + if (command_pid > 0) + { + /* To avoid crashes on SunOS due to bugs in SunOS tmpfs + triggered by the use of rename() in RCS, wait for the + subprocess to die. Unfortunately, this means draining output + while waiting for it to unblock the signal we sent it. Yuck! */ + int status; + pid_t r; + + signal (SIGCHLD, wait_sig); + if (sig) + /* Perhaps SIGTERM would be more correct. But the child + process will delay the SIGINT delivery until its own + children have exited. */ + kill (command_pid, SIGINT); + /* The caller may also have sent a signal to command_pid, so + always try waiting. First, though, check and see if it's still + there.... */ + do_waitpid: + r = waitpid (command_pid, &status, WNOHANG); + if (r == 0) + ; + else if (r == command_pid) + command_pid_is_dead++; + else if (r == -1) + switch (errno) + { + case ECHILD: + command_pid_is_dead++; + break; + case EINTR: + goto do_waitpid; + } + else + /* waitpid should always return one of the above values */ + abort (); + while (!command_pid_is_dead) + { + struct timeval timeout; + struct fd_set_wrapper readfds; + char buf[100]; + int i; + + /* Use a non-zero timeout to avoid eating up CPU cycles. */ + timeout.tv_sec = 2; + timeout.tv_usec = 0; + readfds = command_fds_to_drain; + switch (select (max_command_fd + 1, &readfds.fds, + (fd_set *)0, (fd_set *)0, + &timeout)) + { + case -1: + if (errno != EINTR) + abort (); + case 0: + /* timeout */ + break; + case 1: + for (i = 0; i <= max_command_fd; i++) + { + if (!FD_ISSET (i, &readfds.fds)) + continue; + /* this fd is non-blocking */ + while (read (i, buf, sizeof (buf)) >= 1) + ; + } + break; + default: + abort (); + } + } + } +#endif + + CVS_CHDIR (Tmpdir); + /* Temporarily clear noexec, so that we clean up our temp directory + regardless of it (this could more cleanly be handled by moving + the noexec check to all the unlink_file_dir callers from + unlink_file_dir itself). */ + save_noexec = noexec; + noexec = 0; + /* FIXME? Would be nice to not ignore errors. But what should we do? + We could try to do this before we shut down the network connection, + and try to notify the client (but the client might not be waiting + for responses). We could try something like syslog() or our own + log file. */ + unlink_file_dir (orig_server_temp_dir); + noexec = save_noexec; + + if (buf_to_net != NULL) + (void) buf_shutdown (buf_to_net); +} + +int server_active = 0; +int server_expanding = 0; + +int +server (argc, argv) + int argc; + char **argv; +{ + if (argc == -1) + { + static const char *const msg[] = + { + "Usage: %s %s\n", + " Normally invoked by a cvs client on a remote machine.\n", + NULL + }; + usage (msg); + } + /* Ignore argc and argv. They might be from .cvsrc. */ + + buf_to_net = fd_buffer_initialize (STDOUT_FILENO, 0, + outbuf_memory_error); + buf_from_net = stdio_buffer_initialize (stdin, 1, outbuf_memory_error); + + saved_output = buf_nonio_initialize (outbuf_memory_error); + saved_outerr = buf_nonio_initialize (outbuf_memory_error); + + /* Since we're in the server parent process, error should use the + protocol to report error messages. */ + error_use_protocol = 1; + + /* OK, now figure out where we stash our temporary files. */ + { + char *p; + + /* The code which wants to chdir into server_temp_dir is not set + up to deal with it being a relative path. So give an error + for that case. */ + if (!isabsolute (Tmpdir)) + { + pending_error_text = malloc (80 + strlen (Tmpdir)); + if (pending_error_text == NULL) + { + pending_error = ENOMEM; + } + else + { + sprintf (pending_error_text, + "E Value of %s for TMPDIR is not absolute", Tmpdir); + } + /* FIXME: we would like this error to be persistent, that + is, not cleared by print_pending_error. The current client + will exit as soon as it gets an error, but the protocol spec + does not require a client to do so. */ + } + else + { + int status; + + server_temp_dir = malloc (strlen (Tmpdir) + 80); + if (server_temp_dir == NULL) + { + /* + * Strictly speaking, we're not supposed to output anything + * now. But we're about to exit(), give it a try. + */ + printf ("E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket + subsystems on NT and OS2 or dealing with windows + and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (EXIT_FAILURE); + } + strcpy (server_temp_dir, Tmpdir); + + /* Remove a trailing slash from TMPDIR if present. */ + p = server_temp_dir + strlen (server_temp_dir) - 1; + if (*p == '/') + *p = '\0'; + + /* + * I wanted to use cvs-serv/PID, but then you have to worry about + * the permissions on the cvs-serv directory being right. So + * use cvs-servPID. + */ + strcat (server_temp_dir, "/cvs-serv"); + + p = server_temp_dir + strlen (server_temp_dir); + sprintf (p, "%ld", (long) getpid ()); + + orig_server_temp_dir = server_temp_dir; + + /* Create the temporary directory, and set the mode to + 700, to discourage random people from tampering with + it. */ + status = mkdir_p (server_temp_dir); + if (status != 0 && status != EEXIST) + { + if (alloc_pending (80)) + strcpy (pending_error_text, + "E can't create temporary directory"); + pending_error = status; + } +#ifndef CHMOD_BROKEN + else + { + if (chmod (server_temp_dir, S_IRWXU) < 0) + { + int save_errno = errno; + if (alloc_pending (80)) + strcpy (pending_error_text, "\ +E cannot change permissions on temporary directory"); + pending_error = save_errno; + } + } +#endif + } + } + +#ifdef SIGHUP + (void) SIG_register (SIGHUP, server_cleanup); +#endif +#ifdef SIGINT + (void) SIG_register (SIGINT, server_cleanup); +#endif +#ifdef SIGQUIT + (void) SIG_register (SIGQUIT, server_cleanup); +#endif +#ifdef SIGPIPE + (void) SIG_register (SIGPIPE, server_cleanup); +#endif +#ifdef SIGTERM + (void) SIG_register (SIGTERM, server_cleanup); +#endif + + /* Now initialize our argument vector (for arguments from the client). */ + + /* Small for testing. */ + argument_vector_size = 1; + argument_vector = + (char **) malloc (argument_vector_size * sizeof (char *)); + if (argument_vector == NULL) + { + /* + * Strictly speaking, we're not supposed to output anything + * now. But we're about to exit(), give it a try. + */ + printf ("E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (EXIT_FAILURE); + } + + argument_count = 1; + /* This gets printed if the client supports an option which the + server doesn't, causing the server to print a usage message. + FIXME: probably should be using program_name here. + FIXME: just a nit, I suppose, but the usage message the server + prints isn't literally true--it suggests "cvs server" followed + by options which are for a particular command. Might be nice to + say something like "client apparently supports an option not supported + by this server" or something like that instead of usage message. */ + argument_vector[0] = "cvs server"; + + while (1) + { + char *cmd, *orig_cmd; + struct request *rq; + int status; + + status = buf_read_line (buf_from_net, &cmd, (int *) NULL); + if (status == -2) + { + buf_output0 (buf_to_net, "E Fatal server error, aborting.\n\ +error ENOMEM Virtual memory exhausted.\n"); + break; + } + if (status != 0) + break; + + orig_cmd = cmd; + for (rq = requests; rq->name != NULL; ++rq) + if (strncmp (cmd, rq->name, strlen (rq->name)) == 0) + { + int len = strlen (rq->name); + if (cmd[len] == '\0') + cmd += len; + else if (cmd[len] == ' ') + cmd += len + 1; + else + /* + * The first len characters match, but it's a different + * command. e.g. the command is "cooperate" but we matched + * "co". + */ + continue; + + if (!(rq->flags & RQ_ROOTLESS) + && CVSroot_directory == NULL) + { + /* For commands which change the way in which data + is sent and received, for example Gzip-stream, + this does the wrong thing. Since the client + assumes that everything is being compressed, + unconditionally, there is no way to give this + error to the client without turning on + compression. The obvious fix would be to make + Gzip-stream RQ_ROOTLESS (with the corresponding + change to the spec), and that might be a good + idea but then again I can see some settings in + CVSROOT about what compression level to allow. + I suppose a more baroque answer would be to + turn on compression (say, at level 1), just + enough to give the "Root request missing" + error. For now we just lose. */ + if (alloc_pending (80)) + sprintf (pending_error_text, + "E Protocol error: Root request missing"); + } + else + (*rq->func) (cmd); + break; + } + if (rq->name == NULL) + { + if (!print_pending_error ()) + { + buf_output0 (buf_to_net, "error unrecognized request `"); + buf_output0 (buf_to_net, cmd); + buf_append_char (buf_to_net, '\''); + buf_append_char (buf_to_net, '\n'); + } + } + free (orig_cmd); + } + server_cleanup (0); + return 0; +} + + +#if defined (HAVE_KERBEROS) || defined (AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI) +static void switch_to_user PROTO((const char *)); + +static void +switch_to_user (username) + const char *username; +{ + struct passwd *pw; + + pw = getpwnam (username); + if (pw == NULL) + { + /* Normally this won't be reached; check_password contains + a similar check. */ + + printf ("E Fatal error, aborting.\n\ +error 0 %s: no such user\n", username); + /* Don't worry about server_cleanup; server_active isn't set yet. */ + error_exit (); + } + +#if HAVE_INITGROUPS + if (initgroups (pw->pw_name, pw->pw_gid) < 0 +# ifdef EPERM + /* At least on the system I tried, initgroups() only works as root. + But we do still want to report ENOMEM and whatever other + errors initgroups() might dish up. */ + && errno != EPERM +# endif + ) + { + /* This could be a warning, but I'm not sure I see the point + in doing that instead of an error given that it would happen + on every connection. We could log it somewhere and not tell + the user. But at least for now make it an error. */ + printf ("error 0 initgroups failed: %s\n", strerror (errno)); + /* Don't worry about server_cleanup; server_active isn't set yet. */ + error_exit (); + } +#endif /* HAVE_INITGROUPS */ + +#ifdef SETXID_SUPPORT + /* honor the setgid bit iff set*/ + if (getgid() != getegid()) + { + if (setgid (getegid ()) < 0) + { + /* See comments at setuid call below for more discussion. */ + printf ("error 0 setgid failed: %s\n", strerror (errno)); + /* Don't worry about server_cleanup; + server_active isn't set yet. */ + error_exit (); + } + } + else +#endif + { + if (setgid (pw->pw_gid) < 0) + { + /* See comments at setuid call below for more discussion. */ + printf ("error 0 setgid failed: %s\n", strerror (errno)); + /* Don't worry about server_cleanup; + server_active isn't set yet. */ + error_exit (); + } + } + + if (setuid (pw->pw_uid) < 0) + { + /* Note that this means that if run as a non-root user, + CVSROOT/passwd must contain the user we are running as + (e.g. "joe:FsEfVcu:cvs" if run as "cvs" user). This seems + cleaner than ignoring the error like CVS 1.10 and older but + it does mean that some people might need to update their + CVSROOT/passwd file. */ + printf ("error 0 setuid failed: %s\n", strerror (errno)); + /* Don't worry about server_cleanup; server_active isn't set yet. */ + error_exit (); + } + + /* We don't want our umask to change file modes. The modes should + be set by the modes used in the repository, and by the umask of + the client. */ + umask (0); + +#if HAVE_PUTENV + /* Set LOGNAME and USER in the environment, in case they are + already set to something else. */ + { + char *env; + + env = xmalloc (sizeof "LOGNAME=" + strlen (username)); + (void) sprintf (env, "LOGNAME=%s", username); + (void) putenv (env); + + env = xmalloc (sizeof "USER=" + strlen (username)); + (void) sprintf (env, "USER=%s", username); + (void) putenv (env); + } +#endif /* HAVE_PUTENV */ +} +#endif + +#ifdef AUTH_SERVER_SUPPORT + +extern char *crypt PROTO((const char *, const char *)); + + +/* + * 0 means no entry found for this user. + * 1 means entry found and password matches (or found password is empty) + * 2 means entry found, but password does not match. + * + * If 1, host_user_ptr will be set to point at the system + * username (i.e., the "real" identity, which may or may not be the + * CVS username) of this user; caller may free this. Global + * CVS_Username will point at an allocated copy of cvs username (i.e., + * the username argument below). + * kff todo: FIXME: last sentence is not true, it applies to caller. + */ +static int +check_repository_password (username, password, repository, host_user_ptr) + char *username, *password, *repository, **host_user_ptr; +{ + int retval = 0; + FILE *fp; + char *filename; + char *linebuf = NULL; + size_t linebuf_len; + int found_it = 0; + int namelen; + + /* We don't use CVSroot_directory because it hasn't been set yet + * -- our `repository' argument came from the authentication + * protocol, not the regular CVS protocol. + */ + + filename = xmalloc (strlen (repository) + + 1 + + strlen (CVSROOTADM) + + 1 + + strlen (CVSROOTADM_PASSWD) + + 1); + + (void) sprintf (filename, "%s/%s/%s", repository, + CVSROOTADM, CVSROOTADM_PASSWD); + + fp = CVS_FOPEN (filename, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot open %s", filename); + return 0; + } + + /* Look for a relevant line -- one with this user's name. */ + namelen = strlen (username); + while (getline (&linebuf, &linebuf_len, fp) >= 0) + { + if ((strncmp (linebuf, username, namelen) == 0) + && (linebuf[namelen] == ':')) + { + found_it = 1; + break; + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", filename); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", filename); + + /* If found_it, then linebuf contains the information we need. */ + if (found_it) + { + char *found_password, *host_user_tmp; + char *non_cvsuser_portion; + + /* We need to make sure lines such as + * + * "username::sysuser\n" + * "username:\n" + * "username: \n" + * + * all result in a found_password of NULL, but we also need to + * make sure that + * + * "username: :sysuser\n" + * "username: <whatever>:sysuser\n" + * + * continues to result in an impossible password. That way, + * an admin would be on safe ground by going in and tacking a + * space onto the front of a password to disable the account + * (a technique some people use to close accounts + * temporarily). + */ + + /* Make `non_cvsuser_portion' contain everything after the CVS + username, but null out any final newline. */ + non_cvsuser_portion = linebuf + namelen; + strtok (non_cvsuser_portion, "\n"); + + /* If there's a colon now, we just want to inch past it. */ + if (strchr (non_cvsuser_portion, ':') == non_cvsuser_portion) + non_cvsuser_portion++; + + /* Okay, after this conditional chain, found_password and + host_user_tmp will have useful values: */ + + if ((non_cvsuser_portion == NULL) + || (strlen (non_cvsuser_portion) == 0) + || ((strspn (non_cvsuser_portion, " \t")) + == strlen (non_cvsuser_portion))) + { + found_password = NULL; + host_user_tmp = NULL; + } + else if (strncmp (non_cvsuser_portion, ":", 1) == 0) + { + found_password = NULL; + host_user_tmp = non_cvsuser_portion + 1; + if (strlen (host_user_tmp) == 0) + host_user_tmp = NULL; + } + else + { + found_password = strtok (non_cvsuser_portion, ":"); + host_user_tmp = strtok (NULL, ":"); + } + + /* Of course, maybe there was no system user portion... */ + if (host_user_tmp == NULL) + host_user_tmp = username; + + /* Verify blank passwords directly, otherwise use crypt(). */ + if ((found_password == NULL) + || ((strcmp (found_password, crypt (password, found_password)) + == 0))) + { + /* Give host_user_ptr permanent storage. */ + *host_user_ptr = xstrdup (host_user_tmp); + retval = 1; + } + else + { + *host_user_ptr = NULL; + retval = 2; + } + } + else /* Didn't find this user, so deny access. */ + { + *host_user_ptr = NULL; + retval = 0; + } + + free (filename); + if (linebuf) + free (linebuf); + + return retval; +} + + +/* Return a hosting username if password matches, else NULL. */ +static char * +check_password (username, password, repository) + char *username, *password, *repository; +{ + int rc; + char *host_user = NULL; + + /* First we see if this user has a password in the CVS-specific + password file. If so, that's enough to authenticate with. If + not, we'll check /etc/passwd. */ + + rc = check_repository_password (username, password, repository, + &host_user); + + if (rc == 2) + return NULL; + + /* else */ + + if (rc == 1) + { + /* host_user already set by reference, so just return. */ + goto handle_return; + } + else if (rc == 0 && system_auth) + { + /* No cvs password found, so try /etc/passwd. */ + + const char *found_passwd = NULL; + struct passwd *pw; +#ifdef HAVE_GETSPNAM + struct spwd *spw; + + spw = getspnam (username); + if (spw != NULL) + { + found_passwd = spw->sp_pwdp; + } +#endif + + if (found_passwd == NULL && (pw = getpwnam (username)) != NULL) + { + found_passwd = pw->pw_passwd; + } + + if (found_passwd == NULL) + { + printf ("E Fatal error, aborting.\n\ +error 0 %s: no such user\n", username); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (EXIT_FAILURE); + } + + if (*found_passwd) + { + /* user exists and has a password */ + host_user = ((! strcmp (found_passwd, + crypt (password, found_passwd))) + ? username : NULL); + goto handle_return; + } + else if (password && *password) + { + /* user exists and has no system password, but we got + one as parameter */ + host_user = username; + goto handle_return; + } + else + { + /* user exists but has no password at all */ + host_user = NULL; + goto handle_return; + } + } + else if (rc == 0) + { + /* Note that the message _does_ distinguish between the case in + which we check for a system password and the case in which + we do not. It is a real pain to track down why it isn't + letting you in if it won't say why, and I am not convinced + that the potential information disclosure to an attacker + outweighs this. */ + printf ("error 0 no such user %s in CVSROOT/passwd\n", username); + + /* I'm doing this manually rather than via error_exit () + because I'm not sure whether we want to call server_cleanup. + Needs more investigation.... */ + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + exit (EXIT_FAILURE); + } + else + { + /* Something strange happened. We don't know what it was, but + we certainly won't grant authorization. */ + host_user = NULL; + goto handle_return; + } + +handle_return: + if (host_user) + { + /* Set CVS_Username here, in allocated space. + It might or might not be the same as host_user. */ + CVS_Username = xmalloc (strlen (username) + 1); + strcpy (CVS_Username, username); + } + + return host_user; +} + +#endif /* AUTH_SERVER_SUPPORT */ + +#if defined (AUTH_SERVER_SUPPORT) || defined (HAVE_GSSAPI) + +/* Read username and password from client (i.e., stdin). + If correct, then switch to run as that user and send an ACK to the + client via stdout, else send NACK and die. */ +void +pserver_authenticate_connection () +{ + char *tmp = NULL; + size_t tmp_allocated = 0; +#ifdef AUTH_SERVER_SUPPORT + char *repository = NULL; + size_t repository_allocated = 0; + char *username = NULL; + size_t username_allocated = 0; + char *password = NULL; + size_t password_allocated = 0; + + char *host_user; + char *descrambled_password; +#endif /* AUTH_SERVER_SUPPORT */ + int verify_and_exit = 0; + + /* The Authentication Protocol. Client sends: + * + * BEGIN AUTH REQUEST\n + * <REPOSITORY>\n + * <USERNAME>\n + * <PASSWORD>\n + * END AUTH REQUEST\n + * + * Server uses above information to authenticate, then sends + * + * I LOVE YOU\n + * + * if it grants access, else + * + * I HATE YOU\n + * + * if it denies access (and it exits if denying). + * + * When the client is "cvs login", the user does not desire actual + * repository access, but would like to confirm the password with + * the server. In this case, the start and stop strings are + * + * BEGIN VERIFICATION REQUEST\n + * + * and + * + * END VERIFICATION REQUEST\n + * + * On a verification request, the server's responses are the same + * (with the obvious semantics), but it exits immediately after + * sending the response in both cases. + * + * Why is the repository sent? Well, note that the actual + * client/server protocol can't start up until authentication is + * successful. But in order to perform authentication, the server + * needs to look up the password in the special CVS passwd file, + * before trying /etc/passwd. So the client transmits the + * repository as part of the "authentication protocol". The + * repository will be redundantly retransmitted later, but that's no + * big deal. + */ + +#ifdef SO_KEEPALIVE + /* Set SO_KEEPALIVE on the socket, so that we don't hang forever + if the client dies while we are waiting for input. */ + { + int on = 1; + + (void) setsockopt (STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, + (char *) &on, sizeof on); + } +#endif + + /* Make sure the protocol starts off on the right foot... */ + if (getline (&tmp, &tmp_allocated, stdin) < 0) + /* FIXME: what? We could try writing error/eof, but chances + are the network connection is dead bidirectionally. log it + somewhere? */ + ; + + if (strcmp (tmp, "BEGIN VERIFICATION REQUEST\n") == 0) + verify_and_exit = 1; + else if (strcmp (tmp, "BEGIN AUTH REQUEST\n") == 0) + ; + else if (strcmp (tmp, "BEGIN GSSAPI REQUEST\n") == 0) + { +#ifdef HAVE_GSSAPI + free (tmp); + gserver_authenticate_connection (); + return; +#else + error (1, 0, "GSSAPI authentication not supported by this server"); +#endif + } + else + error (1, 0, "bad auth protocol start: %s", tmp); + +#ifndef AUTH_SERVER_SUPPORT + + error (1, 0, "Password authentication not supported by this server"); + +#else /* AUTH_SERVER_SUPPORT */ + + /* Get the three important pieces of information in order. */ + /* See above comment about error handling. */ + getline (&repository, &repository_allocated, stdin); + getline (&username, &username_allocated, stdin); + getline (&password, &password_allocated, stdin); + + /* Make them pure. */ + strip_trailing_newlines (repository); + strip_trailing_newlines (username); + strip_trailing_newlines (password); + + /* ... and make sure the protocol ends on the right foot. */ + /* See above comment about error handling. */ + getline (&tmp, &tmp_allocated, stdin); + if (strcmp (tmp, + verify_and_exit ? + "END VERIFICATION REQUEST\n" : "END AUTH REQUEST\n") + != 0) + { + error (1, 0, "bad auth protocol end: %s", tmp); + } + if (!root_allow_ok (repository)) + /* Just give a generic I HATE YOU. This is because CVS 1.9.10 + and older clients do not support "error". Once more recent + clients are more widespread, probably want to fix this (it is + a real pain to track down why it isn't letting you in if it + won't say why, and I am not convinced that the potential + information disclosure to an attacker outweighs this). */ + goto i_hate_you; + + /* OK, now parse the config file, so we can use it to control how + to check passwords. If there was an error parsing the config + file, parse_config already printed an error. We keep going. + Why? Because if we didn't, then there would be no way to check + in a new CVSROOT/config file to fix the broken one! */ + parse_config (repository); + + /* We need the real cleartext before we hash it. */ + descrambled_password = descramble (password); + host_user = check_password (username, descrambled_password, repository); + memset (descrambled_password, 0, strlen (descrambled_password)); + free (descrambled_password); + if (host_user == NULL) + { + i_hate_you: + printf ("I HATE YOU\n"); + fflush (stdout); + + /* Don't worry about server_cleanup, server_active isn't set + yet. */ + error_exit (); + } + + /* Don't go any farther if we're just responding to "cvs login". */ + if (verify_and_exit) + { + printf ("I LOVE YOU\n"); + fflush (stdout); + +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + + exit (0); + } + + /* Set Pserver_Repos so that we can check later that the same + repository is sent in later client/server protocol. */ + Pserver_Repos = xmalloc (strlen (repository) + 1); + strcpy (Pserver_Repos, repository); + + /* Switch to run as this user. */ + switch_to_user (host_user); + free (tmp); + free (repository); + free (username); + free (password); + + printf ("I LOVE YOU\n"); + fflush (stdout); +#endif /* AUTH_SERVER_SUPPORT */ +} + +#endif /* AUTH_SERVER_SUPPORT || HAVE_GSSAPI */ + + +#ifdef HAVE_KERBEROS +void +kserver_authenticate_connection () +{ + int status; + char instance[INST_SZ]; + struct sockaddr_in peer; + struct sockaddr_in laddr; + int len; + KTEXT_ST ticket; + AUTH_DAT auth; + char version[KRB_SENDAUTH_VLEN]; + char user[ANAME_SZ]; + + strcpy (instance, "*"); + len = sizeof peer; + if (getpeername (STDIN_FILENO, (struct sockaddr *) &peer, &len) < 0 + || getsockname (STDIN_FILENO, (struct sockaddr *) &laddr, + &len) < 0) + { + printf ("E Fatal error, aborting.\n\ +error %s getpeername or getsockname failed\n", strerror (errno)); +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + exit (EXIT_FAILURE); + } + +#ifdef SO_KEEPALIVE + /* Set SO_KEEPALIVE on the socket, so that we don't hang forever + if the client dies while we are waiting for input. */ + { + int on = 1; + + (void) setsockopt (STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, + (char *) &on, sizeof on); + } +#endif + + status = krb_recvauth (KOPT_DO_MUTUAL, STDIN_FILENO, &ticket, "rcmd", + instance, &peer, &laddr, &auth, "", sched, + version); + if (status != KSUCCESS) + { + printf ("E Fatal error, aborting.\n\ +error 0 kerberos: %s\n", krb_get_err_text(status)); +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + exit (EXIT_FAILURE); + } + + memcpy (kblock, auth.session, sizeof (C_Block)); + + /* Get the local name. */ + status = krb_kntoln (&auth, user); + if (status != KSUCCESS) + { + printf ("E Fatal error, aborting.\n\ +error 0 kerberos: can't get local name: %s\n", krb_get_err_text(status)); +#ifdef SYSTEM_CLEANUP + /* Hook for OS-specific behavior, for example socket subsystems on + NT and OS2 or dealing with windows and arguments on Mac. */ + SYSTEM_CLEANUP (); +#endif + exit (EXIT_FAILURE); + } + + /* Switch to run as this user. */ + switch_to_user (user); +} +#endif /* HAVE_KERBEROS */ + +#ifdef HAVE_GSSAPI + +#ifndef MAXHOSTNAMELEN +#define MAXHOSTNAMELEN (256) +#endif + +/* Authenticate a GSSAPI connection. This is called from + pserver_authenticate_connection, and it handles success and failure + the same way. */ + +static void +gserver_authenticate_connection () +{ + char hostname[MAXHOSTNAMELEN]; + struct hostent *hp; + gss_buffer_desc tok_in, tok_out; + char buf[1024]; + OM_uint32 stat_min, ret; + gss_name_t server_name, client_name; + gss_cred_id_t server_creds; + int nbytes; + gss_OID mechid; + + gethostname (hostname, sizeof hostname); + hp = gethostbyname (hostname); + if (hp == NULL) + error (1, 0, "can't get canonical hostname"); + + sprintf (buf, "cvs@%s", hp->h_name); + tok_in.value = buf; + tok_in.length = strlen (buf); + + if (gss_import_name (&stat_min, &tok_in, GSS_C_NT_HOSTBASED_SERVICE, + &server_name) != GSS_S_COMPLETE) + error (1, 0, "could not import GSSAPI service name %s", buf); + + /* Acquire the server credential to verify the client's + authentication. */ + if (gss_acquire_cred (&stat_min, server_name, 0, GSS_C_NULL_OID_SET, + GSS_C_ACCEPT, &server_creds, + NULL, NULL) != GSS_S_COMPLETE) + error (1, 0, "could not acquire GSSAPI server credentials"); + + gss_release_name (&stat_min, &server_name); + + /* The client will send us a two byte length followed by that many + bytes. */ + if (fread (buf, 1, 2, stdin) != 2) + error (1, errno, "read of length failed"); + + nbytes = ((buf[0] & 0xff) << 8) | (buf[1] & 0xff); + assert (nbytes <= sizeof buf); + + if (fread (buf, 1, nbytes, stdin) != nbytes) + error (1, errno, "read of data failed"); + + gcontext = GSS_C_NO_CONTEXT; + tok_in.length = nbytes; + tok_in.value = buf; + + if (gss_accept_sec_context (&stat_min, + &gcontext, /* context_handle */ + server_creds, /* verifier_cred_handle */ + &tok_in, /* input_token */ + NULL, /* channel bindings */ + &client_name, /* src_name */ + &mechid, /* mech_type */ + &tok_out, /* output_token */ + &ret, + NULL, /* ignore time_rec */ + NULL) /* ignore del_cred_handle */ + != GSS_S_COMPLETE) + { + error (1, 0, "could not verify credentials"); + } + + /* FIXME: Use Kerberos v5 specific code to authenticate to a user. + We could instead use an authentication to access mapping. */ + { + krb5_context kc; + krb5_principal p; + gss_buffer_desc desc; + + krb5_init_context (&kc); + if (gss_display_name (&stat_min, client_name, &desc, + &mechid) != GSS_S_COMPLETE + || krb5_parse_name (kc, ((gss_buffer_t) &desc)->value, &p) != 0 + || krb5_aname_to_localname (kc, p, sizeof buf, buf) != 0 + || krb5_kuserok (kc, p, buf) != TRUE) + { + error (1, 0, "access denied"); + } + krb5_free_principal (kc, p); + krb5_free_context (kc); + } + + if (tok_out.length != 0) + { + char cbuf[2]; + + cbuf[0] = (tok_out.length >> 8) & 0xff; + cbuf[1] = tok_out.length & 0xff; + if (fwrite (cbuf, 1, 2, stdout) != 2 + || (fwrite (tok_out.value, 1, tok_out.length, stdout) + != tok_out.length)) + error (1, errno, "fwrite failed"); + } + + switch_to_user (buf); + + printf ("I LOVE YOU\n"); + fflush (stdout); +} + +#endif /* HAVE_GSSAPI */ + +#endif /* SERVER_SUPPORT */ + +#if defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT) + +/* This global variable is non-zero if the user requests encryption on + the command line. */ +int cvsencrypt; + +/* This global variable is non-zero if the users requests stream + authentication on the command line. */ +int cvsauthenticate; + +#ifdef HAVE_GSSAPI + +/* An buffer interface using GSSAPI. This is built on top of a + packetizing buffer. */ + +/* This structure is the closure field of the GSSAPI translation + routines. */ + +struct cvs_gssapi_wrap_data +{ + /* The GSSAPI context. */ + gss_ctx_id_t gcontext; +}; + +static int cvs_gssapi_wrap_input PROTO((void *, const char *, char *, int)); +static int cvs_gssapi_wrap_output PROTO((void *, const char *, char *, int, + int *)); + +/* Create a GSSAPI wrapping buffer. We use a packetizing buffer with + GSSAPI wrapping routines. */ + +struct buffer * +cvs_gssapi_wrap_buffer_initialize (buf, input, gcontext, memory) + struct buffer *buf; + int input; + gss_ctx_id_t gcontext; + void (*memory) PROTO((struct buffer *)); +{ + struct cvs_gssapi_wrap_data *gd; + + gd = (struct cvs_gssapi_wrap_data *) xmalloc (sizeof *gd); + gd->gcontext = gcontext; + + return (packetizing_buffer_initialize + (buf, + input ? cvs_gssapi_wrap_input : NULL, + input ? NULL : cvs_gssapi_wrap_output, + gd, + memory)); +} + +/* Unwrap data using GSSAPI. */ + +static int +cvs_gssapi_wrap_input (fnclosure, input, output, size) + void *fnclosure; + const char *input; + char *output; + int size; +{ + struct cvs_gssapi_wrap_data *gd = + (struct cvs_gssapi_wrap_data *) fnclosure; + gss_buffer_desc inbuf, outbuf; + OM_uint32 stat_min; + int conf; + + inbuf.value = (void *) input; + inbuf.length = size; + + if (gss_unwrap (&stat_min, gd->gcontext, &inbuf, &outbuf, &conf, NULL) + != GSS_S_COMPLETE) + { + error (1, 0, "gss_unwrap failed"); + } + + if (outbuf.length > size) + abort (); + + memcpy (output, outbuf.value, outbuf.length); + + /* The real packet size is stored in the data, so we don't need to + remember outbuf.length. */ + + gss_release_buffer (&stat_min, &outbuf); + + return 0; +} + +/* Wrap data using GSSAPI. */ + +static int +cvs_gssapi_wrap_output (fnclosure, input, output, size, translated) + void *fnclosure; + const char *input; + char *output; + int size; + int *translated; +{ + struct cvs_gssapi_wrap_data *gd = + (struct cvs_gssapi_wrap_data *) fnclosure; + gss_buffer_desc inbuf, outbuf; + OM_uint32 stat_min; + int conf_req, conf; + + inbuf.value = (void *) input; + inbuf.length = size; + +#ifdef ENCRYPTION + conf_req = cvs_gssapi_encrypt; +#else + conf_req = 0; +#endif + + if (gss_wrap (&stat_min, gd->gcontext, conf_req, GSS_C_QOP_DEFAULT, + &inbuf, &conf, &outbuf) != GSS_S_COMPLETE) + error (1, 0, "gss_wrap failed"); + + /* The packetizing buffer only permits us to add 100 bytes. + FIXME: I don't know what, if anything, is guaranteed by GSSAPI. + This may need to be increased for a different GSSAPI + implementation, or we may need a different algorithm. */ + if (outbuf.length > size + 100) + abort (); + + memcpy (output, outbuf.value, outbuf.length); + + *translated = outbuf.length; + + gss_release_buffer (&stat_min, &outbuf); + + return 0; +} + +#endif /* HAVE_GSSAPI */ + +#ifdef ENCRYPTION + +#ifdef HAVE_KERBEROS + +/* An encryption interface using Kerberos. This is built on top of a + packetizing buffer. */ + +/* This structure is the closure field of the Kerberos translation + routines. */ + +struct krb_encrypt_data +{ + /* The Kerberos key schedule. */ + Key_schedule sched; + /* The Kerberos DES block. */ + C_Block block; +}; + +static int krb_encrypt_input PROTO((void *, const char *, char *, int)); +static int krb_encrypt_output PROTO((void *, const char *, char *, int, + int *)); + +/* Create a Kerberos encryption buffer. We use a packetizing buffer + with Kerberos encryption translation routines. */ + +struct buffer * +krb_encrypt_buffer_initialize (buf, input, sched, block, memory) + struct buffer *buf; + int input; + Key_schedule sched; + C_Block block; + void (*memory) PROTO((struct buffer *)); +{ + struct krb_encrypt_data *kd; + + kd = (struct krb_encrypt_data *) xmalloc (sizeof *kd); + memcpy (kd->sched, sched, sizeof (Key_schedule)); + memcpy (kd->block, block, sizeof (C_Block)); + + return packetizing_buffer_initialize (buf, + input ? krb_encrypt_input : NULL, + input ? NULL : krb_encrypt_output, + kd, + memory); +} + +/* Decrypt Kerberos data. */ + +static int +krb_encrypt_input (fnclosure, input, output, size) + void *fnclosure; + const char *input; + char *output; + int size; +{ + struct krb_encrypt_data *kd = (struct krb_encrypt_data *) fnclosure; + int tcount; + + des_cbc_encrypt ((C_Block *) input, (C_Block *) output, + size, kd->sched, &kd->block, 0); + + /* SIZE is the size of the buffer, which is set by the encryption + routine. The packetizing buffer will arrange for the first two + bytes in the decrypted buffer to be the real (unaligned) + length. As a safety check, make sure that the length in the + buffer corresponds to SIZE. Note that the length in the buffer + is just the length of the data. We must add 2 to account for + the buffer count itself. */ + tcount = ((output[0] & 0xff) << 8) + (output[1] & 0xff); + if (((tcount + 2 + 7) & ~7) != size) + error (1, 0, "Decryption failure"); + + return 0; +} + +/* Encrypt Kerberos data. */ + +static int +krb_encrypt_output (fnclosure, input, output, size, translated) + void *fnclosure; + const char *input; + char *output; + int size; + int *translated; +{ + struct krb_encrypt_data *kd = (struct krb_encrypt_data *) fnclosure; + int aligned; + + /* For security against a known plaintext attack, we should + initialize any padding bytes to random values. Instead, we + just pick up whatever is on the stack, which is at least better + than using zero. */ + + /* Align SIZE to an 8 byte boundary. Note that SIZE includes the + two byte buffer count at the start of INPUT which was added by + the packetizing buffer. */ + aligned = (size + 7) & ~7; + + /* We use des_cbc_encrypt rather than krb_mk_priv because the + latter sticks a timestamp in the block, and krb_rd_priv expects + that timestamp to be within five minutes of the current time. + Given the way the CVS server buffers up data, that can easily + fail over a long network connection. We trust krb_recvauth to + guard against a replay attack. */ + + des_cbc_encrypt ((C_Block *) input, (C_Block *) output, aligned, + kd->sched, &kd->block, 1); + + *translated = aligned; + + return 0; +} + +#endif /* HAVE_KERBEROS */ +#endif /* ENCRYPTION */ +#endif /* defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT) */ + +/* Output LEN bytes at STR. If LEN is zero, then output up to (not including) + the first '\0' byte. */ + +void +cvs_output (str, len) + const char *str; + size_t len; +{ + if (len == 0) + len = strlen (str); +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + buf_output (saved_output, str, len); + buf_copy_lines (buf_to_net, saved_output, 'M'); + } + else if (server_active) + { + buf_output (saved_output, str, len); + buf_copy_lines (protocol, saved_output, 'M'); + buf_send_counted (protocol); + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; + + /* For symmetry with cvs_outerr we would call fflush (stderr) + here. I guess the assumption is that stderr will be + unbuffered, so we don't need to. That sounds like a sound + assumption from the manpage I looked at, but if there was + something fishy about it, my guess is that calling fflush + would not produce a significant performance problem. */ + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stdout); + if (written == 0) + break; + p += written; + to_write -= written; + } + } +} + +/* Output LEN bytes at STR in binary mode. If LEN is zero, then + output zero bytes. */ + +void +cvs_output_binary (str, len) + char *str; + size_t len; +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol || server_active) + { + struct buffer *buf; + char size_text[40]; + + if (error_use_protocol) + buf = buf_to_net; + else + buf = protocol; + + if (!supported_response ("Mbinary")) + { + error (0, 0, "\ +this client does not support writing binary files to stdout"); + return; + } + + buf_output0 (buf, "Mbinary\012"); + sprintf (size_text, "%lu\012", (unsigned long) len); + buf_output0 (buf, size_text); + + /* Not sure what would be involved in using buf_append_data here + without stepping on the toes of our caller (which is responsible + for the memory allocation of STR). */ + buf_output (buf, str, len); + + if (!error_use_protocol) + buf_send_counted (protocol); + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; + + /* For symmetry with cvs_outerr we would call fflush (stderr) + here. I guess the assumption is that stderr will be + unbuffered, so we don't need to. That sounds like a sound + assumption from the manpage I looked at, but if there was + something fishy about it, my guess is that calling fflush + would not produce a significant performance problem. */ +#ifdef USE_SETMODE_STDOUT + int oldmode; + + /* It is possible that this should be the same ifdef as + USE_SETMODE_BINARY but at least for the moment we keep them + separate. Mostly this is just laziness and/or a question + of what has been tested where. Also there might be an + issue of setmode vs. _setmode. */ + /* The Windows doc says to call setmode only right after startup. + I assume that what they are talking about can also be helped + by flushing the stream before changing the mode. */ + fflush (stdout); + oldmode = _setmode (_fileno (stdout), OPEN_BINARY); + if (oldmode < 0) + error (0, errno, "failed to setmode on stdout"); +#endif + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stdout); + if (written == 0) + break; + p += written; + to_write -= written; + } +#ifdef USE_SETMODE_STDOUT + fflush (stdout); + if (_setmode (_fileno (stdout), oldmode) != OPEN_BINARY) + error (0, errno, "failed to setmode on stdout"); +#endif + } +} + +/* Like CVS_OUTPUT but output is for stderr not stdout. */ + +void +cvs_outerr (str, len) + const char *str; + size_t len; +{ + if (len == 0) + len = strlen (str); +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + buf_output (saved_outerr, str, len); + buf_copy_lines (buf_to_net, saved_outerr, 'E'); + } + else if (server_active) + { + buf_output (saved_outerr, str, len); + buf_copy_lines (protocol, saved_outerr, 'E'); + buf_send_counted (protocol); + } + else +#endif + { + size_t written; + size_t to_write = len; + const char *p = str; + + /* Make sure that output appears in order if stdout and stderr + point to the same place. For the server case this is taken + care of by the fact that saved_outerr always holds less + than a line. */ + fflush (stdout); + + while (to_write > 0) + { + written = fwrite (p, 1, to_write, stderr); + if (written == 0) + break; + p += written; + to_write -= written; + } + } +} + +/* Flush stderr. stderr is normally flushed automatically, of course, + but this function is used to flush information from the server back + to the client. */ + +void +cvs_flusherr () +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + /* Flush what we can to the network, but don't block. */ + buf_flush (buf_to_net, 0); + } + else if (server_active) + { + /* Send a special count to tell the parent to flush. */ + buf_send_special_count (protocol, -1); + } + else +#endif + fflush (stderr); +} + +/* Make it possible for the user to see what has been written to + stdout (it is up to the implementation to decide exactly how far it + should go to ensure this). */ + +void +cvs_flushout () +{ +#ifdef SERVER_SUPPORT + if (error_use_protocol) + { + /* Flush what we can to the network, but don't block. */ + buf_flush (buf_to_net, 0); + } + else if (server_active) + { + /* Just do nothing. This is because the code which + cvs_flushout replaces, setting stdout to line buffering in + main.c, didn't get called in the server child process. But + in the future it is quite plausible that we'll want to make + this case work analogously to cvs_flusherr. */ + } + else +#endif + fflush (stdout); +} + +/* Output TEXT, tagging it according to TAG. There are lots more + details about what TAG means in cvsclient.texi but for the simple + case (e.g. non-client/server), TAG is just "newline" to output a + newline (in which case TEXT must be NULL), and any other tag to + output normal text. + + Note that there is no way to output either \0 or \n as part of TEXT. */ + +void +cvs_output_tagged (tag, text) + char *tag; + char *text; +{ + if (text != NULL && strchr (text, '\n') != NULL) + /* Uh oh. The protocol has no way to cope with this. For now + we dump core, although that really isn't such a nice + response given that this probably can be caused by newlines + in filenames and other causes other than bugs in CVS. Note + that we don't want to turn this into "MT newline" because + this case is a newline within a tagged item, not a newline + as extraneous sugar for the user. */ + assert (0); + + /* Start and end tags don't take any text, per cvsclient.texi. */ + if (tag[0] == '+' || tag[0] == '-') + assert (text == NULL); + +#ifdef SERVER_SUPPORT + if (server_active && supported_response ("MT")) + { + struct buffer *buf; + + if (error_use_protocol) + buf = buf_to_net; + else + buf = protocol; + + buf_output0 (buf, "MT "); + buf_output0 (buf, tag); + if (text != NULL) + { + buf_output (buf, " ", 1); + buf_output0 (buf, text); + } + buf_output (buf, "\n", 1); + + if (!error_use_protocol) + buf_send_counted (protocol); + } + else +#endif + { + if (strcmp (tag, "newline") == 0) + cvs_output ("\n", 1); + else if (text != NULL) + cvs_output (text, 0); + } +} diff --git a/gnu/dist/cvs/src/status.c b/gnu/dist/cvs/src/status.c new file mode 100644 index 00000000000..4e5ddcbd658 --- /dev/null +++ b/gnu/dist/cvs/src/status.c @@ -0,0 +1,356 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Status Information + */ + +#include "cvs.h" + +static Dtype status_dirproc PROTO ((void *callerdat, char *dir, + char *repos, char *update_dir, + List *entries)); +static int status_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int tag_list_proc PROTO((Node * p, void *closure)); + +static int local = 0; +static int long_format = 0; +static RCSNode *xrcsnode; + +static const char *const status_usage[] = +{ + "Usage: %s %s [-vlR] [files...]\n", + "\t-v\tVerbose format; includes tag information for the file\n", + "\t-l\tProcess this directory only (not recursive).\n", + "\t-R\tProcess directories recursively.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +cvsstatus (argc, argv) + int argc; + char **argv; +{ + int c; + int err = 0; + + if (argc == -1) + usage (status_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+vlR")) != -1) + { + switch (c) + { + case 'v': + long_format = 1; + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (status_usage); + break; + } + } + argc -= optind; + argv += optind; + + wrap_setup (); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + + ign_setup (); + + if (long_format) + send_arg("-v"); + if (local) + send_arg("-l"); + + /* For a while, we tried setting SEND_NO_CONTENTS here so this + could be a fast operation. That prevents the + server from updating our timestamp if the timestamp is + changed but the file is unmodified. Worse, it is user-visible + (shows "locally modified" instead of "up to date" if + timestamp is changed but file is not). And there is no good + workaround (you might not want to run "cvs update"; "cvs -n + update" doesn't update CVS/Entries; "cvs diff --brief" or + something perhaps could be made to work but somehow that + seems nonintuitive to me even if so). Given that timestamps + seem to have the potential to get munged for any number of + reasons, it seems better to not rely too much on them. */ + + send_files (argc, argv, local, 0, 0); + + send_file_names (argc, argv, SEND_EXPAND_WILD); + + send_to_server ("status\012", 0); + err = get_responses_and_close (); + + return err; + } +#endif + + /* start the recursion processor */ + err = start_recursion (status_fileproc, (FILESDONEPROC) NULL, + status_dirproc, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, + W_LOCAL, 0, 1, (char *) NULL, 1); + + return (err); +} + +/* + * display the status of a file + */ +/* ARGSUSED */ +static int +status_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + Ctype status; + char *sstat; + Vers_TS *vers; + + status = Classify_File (finfo, (char *) NULL, (char *) NULL, (char *) NULL, + 1, 0, &vers, 0); + sstat = "Classify Error"; + switch (status) + { + case T_UNKNOWN: + sstat = "Unknown"; + break; + case T_CHECKOUT: + sstat = "Needs Checkout"; + break; +#ifdef SERVER_SUPPORT + case T_PATCH: + sstat = "Needs Patch"; + break; +#endif + case T_CONFLICT: + /* I _think_ that "unresolved" is correct; that if it has + been resolved then the status will change. But I'm not + sure about that. */ + sstat = "Unresolved Conflict"; + break; + case T_ADDED: + sstat = "Locally Added"; + break; + case T_REMOVED: + sstat = "Locally Removed"; + break; + case T_MODIFIED: + if (vers->ts_conflict) + sstat = "File had conflicts on merge"; + else + sstat = "Locally Modified"; + break; + case T_REMOVE_ENTRY: + sstat = "Entry Invalid"; + break; + case T_UPTODATE: + sstat = "Up-to-date"; + break; + case T_NEEDS_MERGE: + sstat = "Needs Merge"; + break; + case T_TITLE: + /* I don't think this case can occur here. Just print + "Classify Error". */ + break; + } + + cvs_output ("\ +===================================================================\n", 0); + if (vers->ts_user == NULL) + { + cvs_output ("File: no file ", 0); + cvs_output (finfo->file, 0); + cvs_output ("\t\tStatus: ", 0); + cvs_output (sstat, 0); + cvs_output ("\n\n", 0); + } + else + { + char *buf; + buf = xmalloc (strlen (finfo->file) + strlen (sstat) + 80); + sprintf (buf, "File: %-17s\tStatus: %s\n\n", finfo->file, sstat); + cvs_output (buf, 0); + free (buf); + } + + if (vers->vn_user == NULL) + { + cvs_output (" Working revision:\tNo entry for ", 0); + cvs_output (finfo->file, 0); + cvs_output ("\n", 0); + } + else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0') + cvs_output (" Working revision:\tNew file!\n", 0); +#ifdef SERVER_SUPPORT + else if (server_active) + { + cvs_output (" Working revision:\t", 0); + cvs_output (vers->vn_user, 0); + cvs_output ("\n", 0); + } +#endif + else + { + cvs_output (" Working revision:\t", 0); + cvs_output (vers->vn_user, 0); + cvs_output ("\t", 0); + cvs_output (vers->ts_rcs, 0); + cvs_output ("\n", 0); + } + + if (vers->vn_rcs == NULL) + cvs_output (" Repository revision:\tNo revision control file\n", 0); + else + { + cvs_output (" Repository revision:\t", 0); + cvs_output (vers->vn_rcs, 0); + cvs_output ("\t", 0); + cvs_output (vers->srcfile->path, 0); + cvs_output ("\n", 0); + } + + if (vers->entdata) + { + Entnode *edata; + + edata = vers->entdata; + if (edata->tag) + { + if (vers->vn_rcs == NULL) + { + cvs_output (" Sticky Tag:\t\t", 0); + cvs_output (edata->tag, 0); + cvs_output (" - MISSING from RCS file!\n", 0); + } + else + { + if (isdigit ((unsigned char) edata->tag[0])) + { + cvs_output (" Sticky Tag:\t\t", 0); + cvs_output (edata->tag, 0); + cvs_output ("\n", 0); + } + else + { + char *branch = NULL; + + if (RCS_nodeisbranch (finfo->rcs, edata->tag)) + branch = RCS_whatbranch(finfo->rcs, edata->tag); + + cvs_output (" Sticky Tag:\t\t", 0); + cvs_output (edata->tag, 0); + cvs_output (" (", 0); + cvs_output (branch ? "branch" : "revision", 0); + cvs_output (": ", 0); + cvs_output (branch ? branch : vers->vn_rcs, 0); + cvs_output (")\n", 0); + + if (branch) + free (branch); + } + } + } + else if (!really_quiet) + cvs_output (" Sticky Tag:\t\t(none)\n", 0); + + if (edata->date) + { + cvs_output (" Sticky Date:\t\t", 0); + cvs_output (edata->date, 0); + cvs_output ("\n", 0); + } + else if (!really_quiet) + cvs_output (" Sticky Date:\t\t(none)\n", 0); + + if (edata->options && edata->options[0]) + { + cvs_output (" Sticky Options:\t", 0); + cvs_output (edata->options, 0); + cvs_output ("\n", 0); + } + else if (!really_quiet) + cvs_output (" Sticky Options:\t(none)\n", 0); + } + + if (long_format && vers->srcfile) + { + List *symbols = RCS_symbols(vers->srcfile); + + cvs_output ("\n Existing Tags:\n", 0); + if (symbols) + { + xrcsnode = finfo->rcs; + (void) walklist (symbols, tag_list_proc, NULL); + } + else + cvs_output ("\tNo Tags Exist\n", 0); + } + + cvs_output ("\n", 0); + freevers_ts (&vers); + return (0); +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +status_dirproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + char *dir; + char *repos; + char *update_dir; + List *entries; +{ + if (!quiet) + error (0, 0, "Examining %s", update_dir); + return (R_PROCESS); +} + +/* + * Print out a tag and its type + */ +static int +tag_list_proc (p, closure) + Node *p; + void *closure; +{ + char *branch = NULL; + char *buf; + + if (RCS_nodeisbranch (xrcsnode, p->key)) + branch = RCS_whatbranch(xrcsnode, p->key) ; + + buf = xmalloc (80 + strlen (p->key) + + (branch ? strlen (branch) : strlen (p->data))); + sprintf (buf, "\t%-25s\t(%s: %s)\n", p->key, + branch ? "branch" : "revision", + branch ? branch : p->data); + cvs_output (buf, 0); + free (buf); + + if (branch) + free (branch); + + return (0); +} diff --git a/gnu/dist/cvs/src/subr.c b/gnu/dist/cvs/src/subr.c new file mode 100644 index 00000000000..7228d4b2666 --- /dev/null +++ b/gnu/dist/cvs/src/subr.c @@ -0,0 +1,780 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Various useful functions for the CVS support code. + */ + +#include "cvs.h" +#include "getline.h" + +extern char *getlogin (); + +/* + * malloc some data and die if it fails + */ +void * +xmalloc (bytes) + size_t bytes; +{ + char *cp; + + /* Parts of CVS try to xmalloc zero bytes and then free it. Some + systems have a malloc which returns NULL for zero byte + allocations but a free which can't handle NULL, so compensate. */ + if (bytes == 0) + bytes = 1; + + cp = malloc (bytes); + if (cp == NULL) + { + char buf[80]; + sprintf (buf, "out of memory; can not allocate %lu bytes", + (unsigned long) bytes); + error (1, 0, buf); + } + return (cp); +} + +/* + * realloc data and die if it fails [I've always wanted to have "realloc" do + * a "malloc" if the argument is NULL, but you can't depend on it. Here, I + * can *force* it. + */ +void * +xrealloc (ptr, bytes) + void *ptr; + size_t bytes; +{ + char *cp; + + if (!ptr) + cp = malloc (bytes); + else + cp = realloc (ptr, bytes); + + if (cp == NULL) + { + char buf[80]; + sprintf (buf, "out of memory; can not reallocate %lu bytes", + (unsigned long) bytes); + error (1, 0, buf); + } + return (cp); +} + +/* Two constants which tune expand_string. Having MIN_INCR as large + as 1024 might waste a bit of memory, but it shouldn't be too bad + (CVS used to allocate arrays of, say, 3000, PATH_MAX (8192, often), + or other such sizes). Probably anything which is going to allocate + memory which is likely to get as big as MAX_INCR shouldn't be doing + it in one block which must be contiguous, but since getrcskey does + so, we might as well limit the wasted memory to MAX_INCR or so + bytes. + + MIN_INCR and MAX_INCR should both be powers of two and we generally + try to keep our allocations to powers of two for the most part. + Most malloc implementations these days tend to like that. */ + +#define MIN_INCR 1024 +#define MAX_INCR (2*1024*1024) + +/* *STRPTR is a pointer returned from malloc (or NULL), pointing to *N + characters of space. Reallocate it so that points to at least + NEWSIZE bytes of space. Gives a fatal error if out of memory; + if it returns it was successful. */ +void +expand_string (strptr, n, newsize) + char **strptr; + size_t *n; + size_t newsize; +{ + if (*n < newsize) + { + while (*n < newsize) + { + if (*n < MIN_INCR) + *n = MIN_INCR; + else if (*n >= MAX_INCR) + *n += MAX_INCR; + else + { + *n *= 2; + if (*n > MAX_INCR) + *n = MAX_INCR; + } + } + *strptr = xrealloc (*strptr, *n); + } +} + +/* + * Duplicate a string, calling xmalloc to allocate some dynamic space + */ +char * +xstrdup (str) + const char *str; +{ + char *s; + + if (str == NULL) + return ((char *) NULL); + s = xmalloc (strlen (str) + 1); + (void) strcpy (s, str); + return (s); +} + +/* Remove trailing newlines from STRING, destructively. */ +void +strip_trailing_newlines (str) + char *str; +{ + int len; + len = strlen (str) - 1; + + while (str[len] == '\n') + str[len--] = '\0'; +} + +/* Return the number of levels that path ascends above where it starts. + For example: + "../../foo" -> 2 + "foo/../../bar" -> 1 + */ +/* FIXME: Should be using ISDIRSEP, last_component, or some other + mechanism which is more general than just looking at slashes, + particularly for the client.c caller. The server.c caller might + want something different, so be careful. */ +int +pathname_levels (path) + char *path; +{ + char *p; + char *q; + int level; + int max_level; + + max_level = 0; + p = path; + level = 0; + do + { + q = strchr (p, '/'); + if (q != NULL) + ++q; + if (p[0] == '.' && p[1] == '.' && (p[2] == '\0' || p[2] == '/')) + { + --level; + if (-level > max_level) + max_level = -level; + } + else if (p[0] == '.' && (p[1] == '\0' || p[1] == '/')) + ; + else + ++level; + p = q; + } while (p != NULL); + return max_level; +} + + +/* Free a vector, where (*ARGV)[0], (*ARGV)[1], ... (*ARGV)[*PARGC - 1] + are malloc'd and so is *ARGV itself. Such a vector is allocated by + line2argv or expand_wild, for example. */ +void +free_names (pargc, argv) + int *pargc; + char **argv; +{ + register int i; + + for (i = 0; i < *pargc; i++) + { /* only do through *pargc */ + free (argv[i]); + } + free (argv); + *pargc = 0; /* and set it to zero when done */ +} + +/* Convert LINE into arguments separated by SEPCHARS. Set *ARGC + to the number of arguments found, and (*ARGV)[0] to the first argument, + (*ARGV)[1] to the second, etc. *ARGV is malloc'd and so are each of + (*ARGV)[0], (*ARGV)[1], ... Use free_names() to return the memory + allocated here back to the free pool. */ +void +line2argv (pargc, argv, line, sepchars) + int *pargc; + char ***argv; + char *line; + char *sepchars; +{ + char *cp; + /* Could make a case for size_t or some other unsigned type, but + we'll stick with int to avoid signed/unsigned warnings when + comparing with *pargc. */ + int argv_allocated; + + /* Small for testing. */ + /* argv_allocated must be at least 3 because at some places + (e.g. checkout_proc) cvs alters argv[2]. */ + argv_allocated = 4; + *argv = (char **) xmalloc (argv_allocated * sizeof (**argv)); + + *pargc = 0; + for (cp = strtok (line, sepchars); cp; cp = strtok ((char *) NULL, sepchars)) + { + if (*pargc == argv_allocated) + { + argv_allocated *= 2; + *argv = xrealloc (*argv, argv_allocated * sizeof (**argv)); + } + (*argv)[*pargc] = xstrdup (cp); + (*pargc)++; + } +} + +/* + * Returns the number of dots ('.') found in an RCS revision number + */ +int +numdots (s) + const char *s; +{ + int dots = 0; + + for (; *s; s++) + { + if (*s == '.') + dots++; + } + return (dots); +} + +/* Compare revision numbers REV1 and REV2 by consecutive fields. + Return negative, zero, or positive in the manner of strcmp. The + two revision numbers must have the same number of fields, or else + compare_revnums will return an inaccurate result. */ +int +compare_revnums (rev1, rev2) + const char *rev1; + const char *rev2; +{ + const char *s, *sp; + const char *t, *tp; + char *snext, *tnext; + int result = 0; + + sp = s = rev1; + tp = t = rev2; + while (result == 0) + { + result = strtoul (sp, &snext, 10) - strtoul (tp, &tnext, 10); + if (*snext == '\0' || *tnext == '\0') + break; + sp = snext + 1; + tp = tnext + 1; + } + + return result; +} + +char * +increment_revnum (rev) + const char *rev; +{ + char *newrev, *p; + int lastfield; + size_t len = strlen (rev); + + newrev = (char *) xmalloc (len + 2); + memcpy (newrev, rev, len + 1); + p = strrchr (newrev, '.'); + if (p == NULL) + { + free (newrev); + return NULL; + } + lastfield = atoi (++p); + sprintf (p, "%d", lastfield + 1); + + return newrev; +} + +/* Return the username by which the caller should be identified in + CVS, in contexts such as the author field of RCS files, various + logs, etc. */ +char * +getcaller () +{ +#ifndef SYSTEM_GETCALLER + static char *cache; + struct passwd *pw; + uid_t uid; +#endif + + /* If there is a CVS username, return it. */ +#ifdef AUTH_SERVER_SUPPORT + if (CVS_Username != NULL) + return CVS_Username; +#endif + +#ifdef SYSTEM_GETCALLER + return SYSTEM_GETCALLER (); +#else + /* Get the caller's login from his uid. If the real uid is "root" + try LOGNAME USER or getlogin(). If getlogin() and getpwuid() + both fail, return the uid as a string. */ + + if (cache != NULL) + return cache; + + uid = getuid (); + if (uid == (uid_t) 0) + { + char *name; + + /* super-user; try getlogin() to distinguish */ + if (((name = getlogin ()) || (name = getenv("LOGNAME")) || + (name = getenv("USER"))) && *name) + { + cache = xstrdup (name); + return cache; + } + } + if ((pw = (struct passwd *) getpwuid (uid)) == NULL) + { + char uidname[20]; + + (void) sprintf (uidname, "uid%lu", (unsigned long) uid); + cache = xstrdup (uidname); + return cache; + } + cache = xstrdup (pw->pw_name); + return cache; +#endif +} + +#ifdef lint +#ifndef __GNUC__ +/* ARGSUSED */ +time_t +get_date (date, now) + char *date; + struct timeb *now; +{ + time_t foo = 0; + + return (foo); +} +#endif +#endif + +/* Given two revisions, find their greatest common ancestor. If the + two input revisions exist, then rcs guarantees that the gca will + exist. */ + +char * +gca (rev1, rev2) + const char *rev1; + const char *rev2; +{ + int dots; + char *gca; + const char *p[2]; + int j[2]; + char *retval; + + if (rev1 == NULL || rev2 == NULL) + { + error (0, 0, "sanity failure in gca"); + abort(); + } + + /* The greatest common ancestor will have no more dots, and numbers + of digits for each component no greater than the arguments. Therefore + this string will be big enough. */ + gca = xmalloc (strlen (rev1) + strlen (rev2) + 100); + + /* walk the strings, reading the common parts. */ + gca[0] = '\0'; + p[0] = rev1; + p[1] = rev2; + do + { + int i; + char c[2]; + char *s[2]; + + for (i = 0; i < 2; ++i) + { + /* swap out the dot */ + s[i] = strchr (p[i], '.'); + if (s[i] != NULL) { + c[i] = *s[i]; + } + + /* read an int */ + j[i] = atoi (p[i]); + + /* swap back the dot... */ + if (s[i] != NULL) { + *s[i] = c[i]; + p[i] = s[i] + 1; + } + else + { + /* or mark us at the end */ + p[i] = NULL; + } + + } + + /* use the lowest. */ + (void) sprintf (gca + strlen (gca), "%d.", + j[0] < j[1] ? j[0] : j[1]); + + } while (j[0] == j[1] + && p[0] != NULL + && p[1] != NULL); + + /* back up over that last dot. */ + gca[strlen(gca) - 1] = '\0'; + + /* numbers differ, or we ran out of strings. we're done with the + common parts. */ + + dots = numdots (gca); + if (dots == 0) + { + /* revisions differ in trunk major number. */ + + char *q; + const char *s; + + s = (j[0] < j[1]) ? p[0] : p[1]; + + if (s == NULL) + { + /* we only got one number. this is strange. */ + error (0, 0, "bad revisions %s or %s", rev1, rev2); + abort(); + } + else + { + /* we have a minor number. use it. */ + q = gca + strlen (gca); + + *q++ = '.'; + for ( ; *s != '.' && *s != '\0'; ) + *q++ = *s++; + + *q = '\0'; + } + } + else if ((dots & 1) == 0) + { + /* if we have an even number of dots, then we have a branch. + remove the last number in order to make it a revision. */ + + char *s; + + s = strrchr(gca, '.'); + *s = '\0'; + } + + retval = xstrdup (gca); + free (gca); + return retval; +} + +/* Give fatal error if REV is numeric and ARGC,ARGV imply we are + planning to operate on more than one file. The current directory + should be the working directory. Note that callers assume that we + will only be checking the first character of REV; it need not have + '\0' at the end of the tag name and other niceties. Right now this + is only called from admin.c, but if people like the concept it probably + should also be called from diff -r, update -r, get -r, and log -r. */ + +void +check_numeric (rev, argc, argv) + const char *rev; + int argc; + char **argv; +{ + if (rev == NULL || !isdigit ((unsigned char) *rev)) + return; + + /* Note that the check for whether we are processing more than one + file is (basically) syntactic; that is, we don't behave differently + depending on whether a directory happens to contain only a single + file or whether it contains more than one. I strongly suspect this + is the least confusing behavior. */ + if (argc != 1 + || (!wrap_name_has (argv[0], WRAP_TOCVS) && isdir (argv[0]))) + { + error (0, 0, "while processing more than one file:"); + error (1, 0, "attempt to specify a numeric revision"); + } +} + +/* + * Sanity checks and any required fix-up on message passed to RCS via '-m'. + * RCS 5.7 requires that a non-total-whitespace, non-null message be provided + * with '-m'. Returns a newly allocated, non-empty buffer with whitespace + * stripped from end of lines and end of buffer. + * + * TODO: We no longer use RCS to manage repository files, so maybe this + * nonsense about non-empty log fields can be dropped. + */ +char * +make_message_rcslegal (message) + char *message; +{ + char *dst, *dp, *mp; + + if (message == NULL) message = ""; + + /* Strip whitespace from end of lines and end of string. */ + dp = dst = (char *) xmalloc (strlen (message) + 1); + for (mp = message; *mp != '\0'; ++mp) + { + if (*mp == '\n') + { + /* At end-of-line; backtrack to last non-space. */ + while (dp > dst && (dp[-1] == ' ' || dp[-1] == '\t')) + --dp; + } + *dp++ = *mp; + } + + /* Backtrack to last non-space at end of string, and truncate. */ + while (dp > dst && isspace ((unsigned char) dp[-1])) + --dp; + *dp = '\0'; + + /* After all that, if there was no non-space in the string, + substitute a non-empty message. */ + if (*dst == '\0') + { + free (dst); + dst = xstrdup ("*** empty log message ***"); + } + + return dst; +} + +/* Does the file FINFO contain conflict markers? The whole concept + of looking at the contents of the file to figure out whether there are + unresolved conflicts is kind of bogus (people do want to manage files + which contain those patterns not as conflict markers), but for now it + is what we do. */ +int +file_has_markers (finfo) + const struct file_info *finfo; +{ + FILE *fp; + char *line = NULL; + size_t line_allocated = 0; + int result; + + result = 0; + fp = CVS_FOPEN (finfo->file, "r"); + if (fp == NULL) + error (1, errno, "cannot open %s", finfo->fullname); + while (getline (&line, &line_allocated, fp) > 0) + { + if (strncmp (line, RCS_MERGE_PAT, sizeof RCS_MERGE_PAT - 1) == 0) + { + result = 1; + goto out; + } + } + if (ferror (fp)) + error (0, errno, "cannot read %s", finfo->fullname); +out: + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", finfo->fullname); + if (line != NULL) + free (line); + return result; +} + +/* Read the entire contents of the file NAME into *BUF. + If NAME is NULL, read from stdin. *BUF + is a pointer returned from malloc (or NULL), pointing to *BUFSIZE + bytes of space. The actual size is returned in *LEN. On error, + give a fatal error. The name of the file to use in error messages + (typically will include a directory if we have changed directory) + is FULLNAME. MODE is "r" for text or "rb" for binary. */ + +void +get_file (name, fullname, mode, buf, bufsize, len) + const char *name; + const char *fullname; + const char *mode; + char **buf; + size_t *bufsize; + size_t *len; +{ + struct stat s; + size_t nread; + char *tobuf; + FILE *e; + size_t filesize; + + if (name == NULL) + { + e = stdin; + filesize = 100; /* force allocation of minimum buffer */ + } + else + { + /* Although it would be cleaner in some ways to just read + until end of file, reallocating the buffer, this function + does get called on files in the working directory which can + be of arbitrary size, so I think we better do all that + extra allocation. */ + + if (CVS_STAT (name, &s) < 0) + error (1, errno, "can't stat %s", fullname); + + /* Convert from signed to unsigned. */ + filesize = s.st_size; + + e = open_file (name, mode); + } + + if (*bufsize < filesize) + { + *bufsize = filesize; + *buf = xrealloc (*buf, *bufsize); + } + + tobuf = *buf; + nread = 0; + while (1) + { + size_t got; + + got = fread (tobuf, 1, *bufsize - (tobuf - *buf), e); + if (ferror (e)) + error (1, errno, "can't read %s", fullname); + nread += got; + tobuf += got; + + if (feof (e)) + break; + + /* Allocate more space if needed. */ + if (tobuf == *buf + *bufsize) + { + int c; + long off; + + c = getc (e); + if (c == EOF) + break; + off = tobuf - *buf; + expand_string (buf, bufsize, *bufsize + 100); + tobuf = *buf + off; + *tobuf++ = c; + ++nread; + } + } + + if (e != stdin && fclose (e) < 0) + error (0, errno, "cannot close %s", fullname); + + *len = nread; + + /* Force *BUF to be large enough to hold a null terminator. */ + if (*buf != NULL) + { + if (nread == *bufsize) + expand_string (buf, bufsize, *bufsize + 1); + (*buf)[nread] = '\0'; + } +} + + +/* Follow a chain of symbolic links to its destination. FILENAME + should be a handle to a malloc'd block of memory which contains the + beginning of the chain. This routine will replace the contents of + FILENAME with the destination (a real file). */ + +void +resolve_symlink (filename) + char **filename; +{ + if ((! filename) || (! *filename)) + return; + + while (islink (*filename)) + { + char *newname; +#ifdef HAVE_READLINK + /* The clean thing to do is probably to have each filesubr.c + implement this (with an error if not supported by the + platform, in which case islink would presumably return 0). + But that would require editing each filesubr.c and so the + expedient hack seems to be looking at HAVE_READLINK. */ + newname = xreadlink (*filename); +#else + error (1, 0, "internal error: islink doesn't like readlink"); +#endif + + if (isabsolute (newname)) + { + free (*filename); + *filename = newname; + } + else + { + char *oldname = last_component (*filename); + int dirlen = oldname - *filename; + char *fullnewname = xmalloc (dirlen + strlen (newname) + 1); + strncpy (fullnewname, *filename, dirlen); + strcpy (fullnewname + dirlen, newname); + free (newname); + free (*filename); + *filename = fullnewname; + } + } +} + +/* + * Rename a file to an appropriate backup name based on BAKPREFIX. + * If suffix non-null, then ".<suffix>" is appended to the new name. + * + * Returns the new name, which caller may free() if desired. + */ +char * +backup_file (filename, suffix) + const char *filename; + const char *suffix; +{ + char *backup_name; + + if (suffix == NULL) + { + backup_name = xmalloc (sizeof (BAKPREFIX) + strlen (filename) + 1); + sprintf (backup_name, "%s%s", BAKPREFIX, filename); + } + else + { + backup_name = xmalloc (sizeof (BAKPREFIX) + + strlen (filename) + + strlen (suffix) + + 2); /* one for dot, one for trailing '\0' */ + sprintf (backup_name, "%s%s.%s", BAKPREFIX, filename, suffix); + } + + if (isfile (filename)) + copy_file (filename, backup_name); + + return backup_name; +} + diff --git a/gnu/dist/cvs/src/tag.c b/gnu/dist/cvs/src/tag.c new file mode 100644 index 00000000000..fb19cbae78a --- /dev/null +++ b/gnu/dist/cvs/src/tag.c @@ -0,0 +1,929 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * Tag + * + * Add or delete a symbolic name to an RCS file, or a collection of RCS files. + * Uses the checked out revision in the current directory. + */ + +#include "cvs.h" +#include "savecwd.h" + +static int check_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int check_filesdoneproc PROTO ((void *callerdat, int err, + char *repos, char *update_dir, + List *entries)); +static int pretag_proc PROTO((char *repository, char *filter)); +static void masterlist_delproc PROTO((Node *p)); +static void tag_delproc PROTO((Node *p)); +static int pretag_list_proc PROTO((Node *p, void *closure)); + +static Dtype tag_dirproc PROTO ((void *callerdat, char *dir, + char *repos, char *update_dir, + List *entries)); +static int tag_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int tag_filesdoneproc PROTO ((void *callerdat, int err, + char *repos, char *update_dir, + List *entries)); + +static char *numtag; +static char *date = NULL; +static char *symtag; +static int delete_flag; /* adding a tag by default */ +static int branch_mode; /* make an automagic "branch" tag */ +static int local; /* recursive by default */ +static int force_tag_match = 1; /* force tag to match by default */ +static int force_tag_move; /* don't force tag to move by default */ +static int check_uptodate; /* no uptodate-check by default */ + +struct tag_info +{ + Ctype status; + char *rev; + char *tag; + char *options; +}; + +struct master_lists +{ + List *tlist; +}; + +static List *mtlist; +static List *tlist; + +static const char *const tag_usage[] = +{ + "Usage: %s %s [-lRF] [-b] [-d] [-c] [-r rev|-D date] tag [files...]\n", + "\t-l\tLocal directory only, not recursive.\n", + "\t-R\tProcess directories recursively.\n", + "\t-d\tDelete the given tag.\n", + "\t-r rev\tExisting revision/tag.\n", + "\t-D\tExisting date.\n", + "\t-f\tForce a head revision if specified tag not found.\n", + "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n", + "\t-F\tMove tag if it already exists.\n", + "\t-c\tCheck that working files are unmodified.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +int +cvstag (argc, argv) + int argc; + char **argv; +{ + int c; + int err = 0; + + if (argc == -1) + usage (tag_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+FQqlRcdr:D:bf")) != -1) + { + switch (c) + { + case 'Q': + case 'q': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'd': + delete_flag = 1; + break; + case 'c': + check_uptodate = 1; + break; + case 'r': + numtag = optarg; + break; + case 'D': + if (date) + free (date); + date = Make_Date (optarg); + break; + case 'f': + force_tag_match = 0; + break; + case 'b': + branch_mode = 1; + break; + case 'F': + force_tag_move = 1; + break; + case '?': + default: + usage (tag_usage); + break; + } + } + argc -= optind; + argv += optind; + + if (argc == 0) + usage (tag_usage); + symtag = argv[0]; + argc--; + argv++; + + if (date && numtag) + error (1, 0, "-r and -D options are mutually exclusive"); + if (delete_flag && branch_mode) + error (0, 0, "warning: -b ignored with -d options"); + RCS_check_tag (symtag); + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* We're the client side. Fire up the remote server. */ + start_server (); + + ign_setup (); + + if (!force_tag_match) + send_arg ("-f"); + if (local) + send_arg("-l"); + if (delete_flag) + send_arg("-d"); + if (check_uptodate) + send_arg("-c"); + if (branch_mode) + send_arg("-b"); + if (force_tag_move) + send_arg("-F"); + + if (numtag) + option_with_arg ("-r", numtag); + if (date) + client_senddate (date); + + send_arg (symtag); + + send_files (argc, argv, local, 0, + + /* I think the -c case is like "cvs status", in + which we really better be correct rather than + being fast; it is just too confusing otherwise. */ + check_uptodate ? 0 : SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server ("tag\012", 0); + return get_responses_and_close (); + } +#endif + + if (numtag != NULL) + tag_check_valid (numtag, argc, argv, local, 0, ""); + + /* check to make sure they are authorized to tag all the + specified files in the repository */ + + mtlist = getlist(); + err = start_recursion (check_fileproc, check_filesdoneproc, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 1, + (char *) NULL, 1); + + if (err) + { + error (1, 0, "correct the above errors first!"); + } + + /* start the recursion processor */ + err = start_recursion (tag_fileproc, tag_filesdoneproc, tag_dirproc, + (DIRLEAVEPROC) NULL, NULL, argc, argv, local, + W_LOCAL, 0, 0, (char *) NULL, 1); + dellist(&mtlist); + return (err); +} + +/* check file that is to be tagged */ +/* All we do here is add it to our list */ + +static int +check_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *xdir; + Node *p; + Vers_TS *vers; + + if (check_uptodate) + { + Ctype status = Classify_File (finfo, (char *) NULL, (char *) NULL, + (char *) NULL, 1, 0, &vers, 0); + if ((status != T_UPTODATE) && (status != T_CHECKOUT)) + { + error (0, 0, "%s is locally modified", finfo->fullname); + return (1); + } + } + + if (finfo->update_dir[0] == '\0') + xdir = "."; + else + xdir = finfo->update_dir; + if ((p = findnode (mtlist, xdir)) != NULL) + { + tlist = ((struct master_lists *) p->data)->tlist; + } + else + { + struct master_lists *ml; + + tlist = getlist (); + p = getnode (); + p->key = xstrdup (xdir); + p->type = UPDATE; + ml = (struct master_lists *) + xmalloc (sizeof (struct master_lists)); + ml->tlist = tlist; + p->data = (char *) ml; + p->delproc = masterlist_delproc; + (void) addnode (mtlist, p); + } + /* do tlist */ + p = getnode (); + p->key = xstrdup (finfo->file); + p->type = UPDATE; + p->delproc = tag_delproc; + vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); + if (vers->srcfile == NULL) + { + if (!really_quiet) + error (0, 0, "nothing known about %s", finfo->file); + return (1); + } + + /* Here we duplicate the calculation in tag_fileproc about which + version we are going to tag. There probably are some subtle races + (e.g. numtag is "foo" which gets moved between here and + tag_fileproc). */ + if (numtag == NULL && date == NULL) + p->data = xstrdup (vers->vn_user); + else + p->data = RCS_getversion (vers->srcfile, numtag, date, + force_tag_match, NULL); + + if (p->data != NULL) + { + int addit = 1; + char *oversion; + + oversion = RCS_getversion (vers->srcfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (oversion == NULL) + { + if (delete_flag) + { + /* Deleting a tag which did not exist is a noop and + should not be logged. */ + addit = 0; + } + } + else if (delete_flag) + { + free (p->data); + p->data = xstrdup (oversion); + } + else if (strcmp(oversion, p->data) == 0) + { + addit = 0; + } + else if (!force_tag_move) + { + addit = 0; + } + if (oversion != NULL) + { + free(oversion); + } + if (!addit) + { + free(p->data); + p->data = NULL; + } + } + freevers_ts(&vers); + (void) addnode (tlist, p); + return (0); +} + +static int +check_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + char *repos; + char *update_dir; + List *entries; +{ + int n; + Node *p; + + p = findnode(mtlist, update_dir); + if (p != NULL) + { + tlist = ((struct master_lists *) p->data)->tlist; + } + else + { + tlist = (List *) NULL; + } + if ((tlist == NULL) || (tlist->list->next == tlist->list)) + { + return (err); + } + if ((n = Parse_Info(CVSROOTADM_TAGINFO, repos, pretag_proc, 1)) > 0) + { + error (0, 0, "Pre-tag check failed"); + err += n; + } + return (err); +} + +static int +pretag_proc(repository, filter) + char *repository; + char *filter; +{ + if (filter[0] == '/') + { + char *s, *cp; + + s = xstrdup(filter); + for (cp=s; *cp; cp++) + { + if (isspace ((unsigned char) *cp)) + { + *cp = '\0'; + break; + } + } + if (!isfile(s)) + { + error (0, errno, "cannot find pre-tag filter '%s'", s); + free(s); + return (1); + } + free(s); + } + run_setup (filter); + run_arg (symtag); + run_arg (delete_flag ? "del" : force_tag_move ? "mov" : "add"); + run_arg (repository); + walklist(tlist, pretag_list_proc, NULL); + return (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL)); +} + +static void +masterlist_delproc(p) + Node *p; +{ + struct master_lists *ml; + + ml = (struct master_lists *)p->data; + dellist(&ml->tlist); + free(ml); + return; +} + +static void +tag_delproc(p) + Node *p; +{ + if (p->data != NULL) + { + free(p->data); + p->data = NULL; + } + return; +} + +static int +pretag_list_proc(p, closure) + Node *p; + void *closure; +{ + if (p->data != NULL) + { + run_arg(p->key); + run_arg(p->data); + } + return (0); +} + + +/* + * Called to tag a particular file (the currently checked out version is + * tagged with the specified tag - or the specified tag is deleted). + */ +/* ARGSUSED */ +static int +tag_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *version, *oversion; + char *nversion = NULL; + char *rev; + Vers_TS *vers; + int retcode = 0; + + /* Lock the directory if it is not already locked. We can't rely + on tag_dirproc because it won't handle the case where the user + specifies a list of files on the command line. */ + /* We do not need to acquire a full write lock for the tag operation: + the revisions are obtained from the working directory, so we do not + require consistency across the entire repository. However, we do + need to prevent simultaneous tag operations from interfering with + each other. Therefore, we write lock each directory as we enter + it, and unlock it as we leave it. */ + lock_dir_for_write (finfo->repository); + + vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); + + if ((numtag != NULL) || (date != NULL)) + { + nversion = RCS_getversion(vers->srcfile, + numtag, + date, + force_tag_match, + (int *) NULL); + if (nversion == NULL) + { + freevers_ts (&vers); + return (0); + } + } + if (delete_flag) + { + + /* + * If -d is specified, "force_tag_match" is set, so that this call to + * RCS_getversion() will return a NULL version string if the symbolic + * tag does not exist in the RCS file. + * + * This is done here because it's MUCH faster than just blindly calling + * "rcs" to remove the tag... trust me. + */ + + version = RCS_getversion (vers->srcfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (version == NULL || vers->srcfile == NULL) + { + freevers_ts (&vers); + return (0); + } + free (version); + + if ((retcode = RCS_deltag(vers->srcfile, symtag)) != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to remove tag %s from %s", symtag, + vers->srcfile->path); + freevers_ts (&vers); + return (1); + } + RCS_rewrite (vers->srcfile, NULL, NULL); + + /* warm fuzzies */ + if (!really_quiet) + { + cvs_output ("D ", 2); + cvs_output (finfo->fullname, 0); + cvs_output ("\n", 1); + } + + freevers_ts (&vers); + return (0); + } + + /* + * If we are adding a tag, we need to know which version we have checked + * out and we'll tag that version. + */ + if (nversion == NULL) + { + version = vers->vn_user; + } + else + { + version = nversion; + } + if (version == NULL) + { + freevers_ts (&vers); + return (0); + } + else if (strcmp (version, "0") == 0) + { + if (!quiet) + error (0, 0, "couldn't tag added but un-commited file `%s'", finfo->file); + freevers_ts (&vers); + return (0); + } + else if (version[0] == '-') + { + if (!quiet) + error (0, 0, "skipping removed but un-commited file `%s'", finfo->file); + freevers_ts (&vers); + return (0); + } + else if (vers->srcfile == NULL) + { + if (!quiet) + error (0, 0, "cannot find revision control file for `%s'", finfo->file); + freevers_ts (&vers); + return (0); + } + + /* + * As an enhancement for the case where a tag is being re-applied to a + * large number of files, make one extra call to RCS_getversion to see + * if the tag is already set in the RCS file. If so, check to see if it + * needs to be moved. If not, do nothing. This will likely save a lot of + * time when simply moving the tag to the "current" head revisions of a + * module -- which I have found to be a typical tagging operation. + */ + rev = branch_mode ? RCS_magicrev (vers->srcfile, version) : version; + oversion = RCS_getversion (vers->srcfile, symtag, (char *) NULL, 1, + (int *) NULL); + if (oversion != NULL) + { + int isbranch = RCS_nodeisbranch (finfo->rcs, symtag); + + /* + * if versions the same and neither old or new are branches don't have + * to do anything + */ + if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch) + { + free (oversion); + freevers_ts (&vers); + return (0); + } + + if (!force_tag_move) + { + /* we're NOT going to move the tag */ + cvs_output ("W ", 2); + cvs_output (finfo->fullname, 0); + cvs_output (" : ", 0); + cvs_output (symtag, 0); + cvs_output (" already exists on ", 0); + cvs_output (isbranch ? "branch" : "version", 0); + cvs_output (" ", 0); + cvs_output (oversion, 0); + cvs_output (" : NOT MOVING tag to ", 0); + cvs_output (branch_mode ? "branch" : "version", 0); + cvs_output (" ", 0); + cvs_output (rev, 0); + cvs_output ("\n", 1); + free (oversion); + freevers_ts (&vers); + return (0); + } + free (oversion); + } + + if ((retcode = RCS_settag(vers->srcfile, symtag, rev)) != 0) + { + error (1, retcode == -1 ? errno : 0, + "failed to set tag %s to revision %s in %s", + symtag, rev, vers->srcfile->path); + freevers_ts (&vers); + return (1); + } + RCS_rewrite (vers->srcfile, NULL, NULL); + + /* more warm fuzzies */ + if (!really_quiet) + { + cvs_output ("T ", 2); + cvs_output (finfo->fullname, 0); + cvs_output ("\n", 1); + } + + if (nversion != NULL) + { + free (nversion); + } + freevers_ts (&vers); + return (0); +} + +/* Clear any lock we may hold on the current directory. */ + +static int +tag_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + char *repos; + char *update_dir; + List *entries; +{ + Lock_Cleanup (); + + return (err); +} + +/* + * Print a warm fuzzy message + */ +/* ARGSUSED */ +static Dtype +tag_dirproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + char *dir; + char *repos; + char *update_dir; + List *entries; +{ + if (!quiet) + error (0, 0, "%s %s", delete_flag ? "Untagging" : "Tagging", update_dir); + return (R_PROCESS); +} + +/* Code relating to the val-tags file. Note that this file has no way + of knowing when a tag has been deleted. The problem is that there + is no way of knowing whether a tag still exists somewhere, when we + delete it some places. Using per-directory val-tags files (in + CVSREP) might be better, but that might slow down the process of + verifying that a tag is correct (maybe not, for the likely cases, + if carefully done), and/or be harder to implement correctly. */ + +struct val_args { + char *name; + int found; +}; + +static int val_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +static int +val_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + RCSNode *rcsdata; + struct val_args *args = (struct val_args *)callerdat; + char *tag; + + if ((rcsdata = finfo->rcs) == NULL) + /* Not sure this can happen, after all we passed only + W_REPOS | W_ATTIC. */ + return 0; + + tag = RCS_gettag (rcsdata, args->name, 1, (int *) NULL); + if (tag != NULL) + { + /* FIXME: should find out a way to stop the search at this point. */ + args->found = 1; + free (tag); + } + return 0; +} + +static Dtype val_direntproc PROTO ((void *, char *, char *, char *, List *)); + +static Dtype +val_direntproc (callerdat, dir, repository, update_dir, entries) + void *callerdat; + char *dir; + char *repository; + char *update_dir; + List *entries; +{ + /* This is not quite right--it doesn't get right the case of "cvs + update -d -r foobar" where foobar is a tag which exists only in + files in a directory which does not exist yet, but which is + about to be created. */ + if (isdir (dir)) + return 0; + return R_SKIP_ALL; +} + +/* Check to see whether NAME is a valid tag. If so, return. If not + print an error message and exit. ARGC, ARGV, LOCAL, and AFLAG specify + which files we will be operating on. + + REPOSITORY is the repository if we need to cd into it, or NULL if + we are already there, or "" if we should do a W_LOCAL recursion. + Sorry for three cases, but the "" case is needed in case the + working directories come from diverse parts of the repository, the + NULL case avoids an unneccesary chdir, and the non-NULL, non-"" + case is needed for checkout, where we don't want to chdir if the + tag is found in CVSROOTADM_VALTAGS, but there is not (yet) any + local directory. */ +void +tag_check_valid (name, argc, argv, local, aflag, repository) + char *name; + int argc; + char **argv; + int local; + int aflag; + char *repository; +{ + DBM *db; + char *valtags_filename; + int err; + datum mytag; + struct val_args the_val_args; + struct saved_cwd cwd; + int which; + + /* Numeric tags require only a syntactic check. */ + if (isdigit ((unsigned char) name[0])) + { + char *p; + for (p = name; *p != '\0'; ++p) + { + if (!(isdigit ((unsigned char) *p) || *p == '.')) + error (1, 0, "\ +Numeric tag %s contains characters other than digits and '.'", name); + } + return; + } + + /* Special tags are always valid. */ + if (strcmp (name, TAG_BASE) == 0 + || strcmp (name, TAG_HEAD) == 0) + return; + + /* FIXME: This routine doesn't seem to do any locking whatsoever + (and it is called from places which don't have locks in place). + If two processes try to write val-tags at the same time, it would + seem like we are in trouble. */ + + mytag.dptr = name; + mytag.dsize = strlen (name); + + valtags_filename = xmalloc (strlen (CVSroot_directory) + + sizeof CVSROOTADM + + sizeof CVSROOTADM_VALTAGS + 20); + strcpy (valtags_filename, CVSroot_directory); + strcat (valtags_filename, "/"); + strcat (valtags_filename, CVSROOTADM); + strcat (valtags_filename, "/"); + strcat (valtags_filename, CVSROOTADM_VALTAGS); + db = dbm_open (valtags_filename, O_RDWR, 0666); + if (db == NULL) + { + if (!existence_error (errno)) + error (1, errno, "cannot read %s", valtags_filename); + + /* If the file merely fails to exist, we just keep going and create + it later if need be. */ + } + else + { + datum val; + + val = dbm_fetch (db, mytag); + if (val.dptr != NULL) + { + /* Found. The tag is valid. */ + dbm_close (db); + free (valtags_filename); + return; + } + /* FIXME: should check errors somehow (add dbm_error to myndbm.c?). */ + } + + /* We didn't find the tag in val-tags, so look through all the RCS files + to see whether it exists there. Yes, this is expensive, but there + is no other way to cope with a tag which might have been created + by an old version of CVS, from before val-tags was invented. + + Since we need this code anyway, we also use it to create + entries in val-tags in general (that is, the val-tags entry + will get created the first time the tag is used, not when the + tag is created). */ + + the_val_args.name = name; + the_val_args.found = 0; + + which = W_REPOS | W_ATTIC; + + if (repository != NULL) + { + if (repository[0] == '\0') + which |= W_LOCAL; + else + { + if (save_cwd (&cwd)) + error_exit (); + if ( CVS_CHDIR (repository) < 0) + error (1, errno, "cannot change to %s directory", repository); + } + } + + err = start_recursion (val_fileproc, (FILESDONEPROC) NULL, + val_direntproc, (DIRLEAVEPROC) NULL, + (void *)&the_val_args, + argc, argv, local, which, aflag, + 1, NULL, 1); + if (repository != NULL && repository[0] != '\0') + { + if (restore_cwd (&cwd, NULL)) + exit (EXIT_FAILURE); + free_cwd (&cwd); + } + + if (!the_val_args.found) + error (1, 0, "no such tag %s", name); + else + { + /* The tags is valid but not mentioned in val-tags. Add it. */ + datum value; + + if (noexec) + { + if (db != NULL) + dbm_close (db); + free (valtags_filename); + return; + } + + if (db == NULL) + { + mode_t omask; + omask = umask (cvsumask); + db = dbm_open (valtags_filename, O_RDWR | O_CREAT | O_TRUNC, 0666); + (void) umask (omask); + + if (db == NULL) + { + error (0, errno, "cannot create %s", valtags_filename); + free (valtags_filename); + return; + } + } + value.dptr = "y"; + value.dsize = 1; + if (dbm_store (db, mytag, value, DBM_REPLACE) < 0) + error (0, errno, "cannot store %s into %s", name, + valtags_filename); + dbm_close (db); + } + free (valtags_filename); +} + +/* + * Check whether a join tag is valid. This is just like + * tag_check_valid, but we must stop before the colon if there is one. + */ + +void +tag_check_valid_join (join_tag, argc, argv, local, aflag, repository) + char *join_tag; + int argc; + char **argv; + int local; + int aflag; + char *repository; +{ + char *c, *s; + + c = xstrdup (join_tag); + s = strchr (c, ':'); + if (s != NULL) + { + if (isdigit ((unsigned char) join_tag[0])) + error (1, 0, + "Numeric join tag %s may not contain a date specifier", + join_tag); + + *s = '\0'; + /* hmmm... I think it makes sense to allow -j:<date>, but + * for now this fixes a bug where CVS just spins and spins (I + * think in the RCS code) looking for a zero length tag. + */ + if (!*c) + error (1, 0, + "argument to join may not contain a date specifier without a tag"); + } + + tag_check_valid (c, argc, argv, local, aflag, repository); + + free (c); +} diff --git a/gnu/dist/cvs/src/update.c b/gnu/dist/cvs/src/update.c new file mode 100644 index 00000000000..61d84d43737 --- /dev/null +++ b/gnu/dist/cvs/src/update.c @@ -0,0 +1,2861 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + * + * "update" updates the version in the present directory with respect to the RCS + * repository. The present version must have been created by "checkout". The + * user can keep up-to-date by calling "update" whenever he feels like it. + * + * The present version can be committed by "commit", but this keeps the version + * in tact. + * + * Arguments following the options are taken to be file names to be updated, + * rather than updating the entire directory. + * + * Modified or non-existent RCS files are checked out and reported as U + * <user_file> + * + * Modified user files are reported as M <user_file>. If both the RCS file and + * the user file have been modified, the user file is replaced by the result + * of rcsmerge, and a backup file is written for the user in .#file.version. + * If this throws up irreconcilable differences, the file is reported as C + * <user_file>, and as M <user_file> otherwise. + * + * Files added but not yet committed are reported as A <user_file>. Files + * removed but not yet committed are reported as R <user_file>. + * + * If the current directory contains subdirectories that hold concurrent + * versions, these are updated too. If the -d option was specified, new + * directories added to the repository are automatically created and updated + * as well. + */ + +#include "cvs.h" +#include "savecwd.h" +#ifdef SERVER_SUPPORT +#include "md5.h" +#endif +#include "watch.h" +#include "fileattr.h" +#include "edit.h" +#include "getline.h" +#include "buffer.h" +#include "hardlink.h" + +static int checkout_file PROTO ((struct file_info *finfo, Vers_TS *vers_ts, + int adding, int merging, int update_server)); +#ifdef SERVER_SUPPORT +static void checkout_to_buffer PROTO ((void *, const char *, size_t)); +#endif +#ifdef SERVER_SUPPORT +static int patch_file PROTO ((struct file_info *finfo, + Vers_TS *vers_ts, + int *docheckout, struct stat *file_info, + unsigned char *checksum)); +static void patch_file_write PROTO ((void *, const char *, size_t)); +#endif +static int merge_file PROTO ((struct file_info *finfo, Vers_TS *vers)); +static int scratch_file PROTO((struct file_info *finfo)); +static Dtype update_dirent_proc PROTO ((void *callerdat, char *dir, + char *repository, char *update_dir, + List *entries)); +static int update_dirleave_proc PROTO ((void *callerdat, char *dir, + int err, char *update_dir, + List *entries)); +static int update_fileproc PROTO ((void *callerdat, struct file_info *)); +static int update_filesdone_proc PROTO ((void *callerdat, int err, + char *repository, char *update_dir, + List *entries)); +#ifdef PRESERVE_PERMISSIONS_SUPPORT +static int get_linkinfo_proc PROTO ((void *callerdat, struct file_info *)); +#endif +static void write_letter PROTO ((struct file_info *finfo, int letter)); +static void join_file PROTO ((struct file_info *finfo, Vers_TS *vers_ts)); + +static char *options = NULL; +static char *tag = NULL; +static char *date = NULL; +/* This is a bit of a kludge. We call WriteTag at the beginning + before we know whether nonbranch is set or not. And then at the + end, once we have the right value for nonbranch, we call WriteTag + again. I don't know whether the first call is necessary or not. + rewrite_tag is nonzero if we are going to have to make that second + call. */ +static int rewrite_tag; +static int nonbranch; + +/* If we set the tag or date for a subdirectory, we use this to undo + the setting. See update_dirent_proc. */ +static char *tag_update_dir; + +static char *join_rev1, *date_rev1; +static char *join_rev2, *date_rev2; +static int aflag = 0; +static int toss_local_changes = 0; +static int force_tag_match = 1; +static int update_build_dirs = 0; +static int update_prune_dirs = 0; +static int pipeout = 0; +#ifdef SERVER_SUPPORT +static int patches = 0; +static int rcs_diff_patches = 0; +#endif +static List *ignlist = (List *) NULL; +static time_t last_register_time; +static const char *const update_usage[] = +{ + "Usage: %s %s [-APdflRp] [-k kopt] [-r rev|-D date] [-j rev]\n", + " [-I ign] [-W spec] [files...]\n", + "\t-A\tReset any sticky tags/date/kopts.\n", + "\t-P\tPrune empty directories.\n", + "\t-C\tOverwrite locally modified files with clean repository copies.\n", + "\t-d\tBuild directories, like checkout does.\n", + "\t-f\tForce a head revision match if tag/date not found.\n", + "\t-l\tLocal directory only, no recursion.\n", + "\t-R\tProcess directories recursively.\n", + "\t-p\tSend updates to standard output (avoids stickiness).\n", + "\t-k kopt\tUse RCS kopt -k option on checkout.\n", + "\t-r rev\tUpdate using specified revision/tag (is sticky).\n", + "\t-D date\tSet date to update from (is sticky).\n", + "\t-j rev\tMerge in changes made between current revision and rev.\n", + "\t-I ign\tMore files to ignore (! to reset).\n", + "\t-W spec\tWrappers specification line.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +/* + * update is the argv,argc based front end for arg parsing + */ +int +update (argc, argv) + int argc; + char **argv; +{ + int c, err; + int local = 0; /* recursive by default */ + int which; /* where to look for files and dirs */ + + if (argc == -1) + usage (update_usage); + + ign_setup (); + wrap_setup (); + + /* parse the args */ + optind = 0; + while ((c = getopt (argc, argv, "+ApCPflRQqduk:r:D:j:I:W:")) != -1) + { + switch (c) + { + case 'A': + aflag = 1; + break; + case 'C': + toss_local_changes = 1; + break; + case 'I': + ign_add (optarg, 0); + break; + case 'W': + wrap_add (optarg, 0); + break; + case 'k': + if (options) + free (options); + options = RCS_check_kflag (optarg); + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'Q': + case 'q': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'd': + update_build_dirs = 1; + break; + case 'f': + force_tag_match = 0; + break; + case 'r': + tag = optarg; + break; + case 'D': + date = Make_Date (optarg); + break; + case 'P': + update_prune_dirs = 1; + break; + case 'p': + pipeout = 1; + noexec = 1; /* so no locks will be created */ + break; + case 'j': + if (join_rev2) + error (1, 0, "only two -j options can be specified"); + if (join_rev1) + join_rev2 = optarg; + else + join_rev1 = optarg; + break; + case 'u': +#ifdef SERVER_SUPPORT + if (server_active) + { + patches = 1; + rcs_diff_patches = server_use_rcs_diff (); + } + else +#endif + usage (update_usage); + break; + case '?': + default: + usage (update_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + int pass; + + /* The first pass does the regular update. If we receive at least + one patch which failed, we do a second pass and just fetch + those files whose patches failed. */ + pass = 1; + do + { + int status; + + start_server (); + + if (local) + send_arg("-l"); + if (update_build_dirs) + send_arg("-d"); + if (pipeout) + send_arg("-p"); + if (!force_tag_match) + send_arg("-f"); + if (aflag) + send_arg("-A"); + if (toss_local_changes) + send_arg("-C"); + if (update_prune_dirs) + send_arg("-P"); + client_prune_dirs = update_prune_dirs; + option_with_arg ("-r", tag); + if (options && options[0] != '\0') + send_arg (options); + if (date) + client_senddate (date); + if (join_rev1) + option_with_arg ("-j", join_rev1); + if (join_rev2) + option_with_arg ("-j", join_rev2); + wrap_send (); + + /* If the server supports the command "update-patches", that means + that it knows how to handle the -u argument to update, which + means to send patches instead of complete files. + + We don't send -u if failed_patches != NULL, so that the + server doesn't try to send patches which will just fail + again. At least currently, the client also clobbers the + file and tells the server it is lost, which also will get + a full file instead of a patch, but it seems clean to omit + -u. */ + if (failed_patches == NULL) + { + if (supported_request ("update-patches")) + send_arg ("-u"); + } + + if (failed_patches == NULL) + { + unsigned int flags = 0; + + if (update_build_dirs) + flags |= SEND_BUILD_DIRS; + + if (toss_local_changes) { + flags |= SEND_NO_CONTENTS; + flags |= BACKUP_MODIFIED_FILES; + } + + /* If noexec, probably could be setting SEND_NO_CONTENTS. + Same caveats as for "cvs status" apply. */ + + send_files (argc, argv, local, aflag, flags); + send_file_names (argc, argv, SEND_EXPAND_WILD); + } + else + { + int i; + + (void) printf ("%s client: refetching unpatchable files\n", + program_name); + + if (toplevel_wd != NULL + && CVS_CHDIR (toplevel_wd) < 0) + { + error (1, errno, "could not chdir to %s", toplevel_wd); + } + + for (i = 0; i < failed_patches_count; i++) + if (unlink_file (failed_patches[i]) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", + failed_patches[i]); + send_files (failed_patches_count, failed_patches, local, + aflag, update_build_dirs ? SEND_BUILD_DIRS : 0); + send_file_names (failed_patches_count, failed_patches, 0); + } + + failed_patches = NULL; + failed_patches_count = 0; + + send_to_server ("update\012", 0); + + status = get_responses_and_close (); + + /* If there are any conflicts, the server will return a + non-zero exit status. If any patches failed, we still + want to run the update again. We use a pass count to + avoid an endless loop. */ + + /* Notes: (1) assuming that status != 0 implies a + potential conflict is the best we can cleanly do given + the current protocol. I suppose that trying to + re-fetch in cases where there was a more serious error + is probably more or less harmless, but it isn't really + ideal. (2) it would be nice to have a testsuite case for the + conflict-and-patch-failed case. */ + + if (status != 0 + && (failed_patches == NULL || pass > 1)) + { + return status; + } + + ++pass; + } while (failed_patches != NULL); + + return 0; + } +#endif + + if (tag != NULL) + tag_check_valid (tag, argc, argv, local, aflag, ""); + if (join_rev1 != NULL) + tag_check_valid_join (join_rev1, argc, argv, local, aflag, ""); + if (join_rev2 != NULL) + tag_check_valid_join (join_rev2, argc, argv, local, aflag, ""); + + /* + * If we are updating the entire directory (for real) and building dirs + * as we go, we make sure there is no static entries file and write the + * tag file as appropriate + */ + if (argc <= 0 && !pipeout) + { + if (update_build_dirs) + { + if (unlink_file (CVSADM_ENTSTAT) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove file %s", CVSADM_ENTSTAT); +#ifdef SERVER_SUPPORT + if (server_active) + server_clear_entstat (".", Name_Repository (NULL, NULL)); +#endif + } + + /* keep the CVS/Tag file current with the specified arguments */ + if (aflag || tag || date) + { + WriteTag ((char *) NULL, tag, date, 0, + ".", Name_Repository (NULL, NULL)); + rewrite_tag = 1; + nonbranch = 0; + } + } + + /* look for files/dirs locally and in the repository */ + which = W_LOCAL | W_REPOS; + + /* look in the attic too if a tag or date is specified */ + if (tag != NULL || date != NULL || joining()) + which |= W_ATTIC; + + /* call the command line interface */ + err = do_update (argc, argv, options, tag, date, force_tag_match, + local, update_build_dirs, aflag, update_prune_dirs, + pipeout, which, join_rev1, join_rev2, (char *) NULL); + + /* free the space Make_Date allocated if necessary */ + if (date != NULL) + free (date); + + return (err); +} + +/* + * Command line interface to update (used by checkout) + */ +int +do_update (argc, argv, xoptions, xtag, xdate, xforce, local, xbuild, xaflag, + xprune, xpipeout, which, xjoin_rev1, xjoin_rev2, preload_update_dir) + int argc; + char **argv; + char *xoptions; + char *xtag; + char *xdate; + int xforce; + int local; + int xbuild; + int xaflag; + int xprune; + int xpipeout; + int which; + char *xjoin_rev1; + char *xjoin_rev2; + char *preload_update_dir; +{ + int err = 0; + char *cp; + + /* fill in the statics */ + options = xoptions; + tag = xtag; + date = xdate; + force_tag_match = xforce; + update_build_dirs = xbuild; + aflag = xaflag; + update_prune_dirs = xprune; + pipeout = xpipeout; + + /* setup the join support */ + join_rev1 = xjoin_rev1; + join_rev2 = xjoin_rev2; + if (join_rev1 && (cp = strchr (join_rev1, ':')) != NULL) + { + *cp++ = '\0'; + date_rev1 = Make_Date (cp); + } + else + date_rev1 = (char *) NULL; + if (join_rev2 && (cp = strchr (join_rev2, ':')) != NULL) + { + *cp++ = '\0'; + date_rev2 = Make_Date (cp); + } + else + date_rev2 = (char *) NULL; + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (preserve_perms) + { + /* We need to do an extra recursion, bleah. It's to make sure + that we know as much as possible about file linkage. */ + hardlist = getlist(); + working_dir = xgetwd(); /* save top-level working dir */ + + /* FIXME-twp: the arguments to start_recursion make me dizzy. This + function call was copied from the update_fileproc call that + follows it; someone should make sure that I did it right. */ + err = start_recursion (get_linkinfo_proc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, which, aflag, 1, + preload_update_dir, 1); + if (err) + return (err); + + /* FIXME-twp: at this point we should walk the hardlist + and update the `links' field of each hardlink_info struct + to list the files that are linked on dist. That would make + it easier & more efficient to compare the disk linkage with + the repository linkage (a simple strcmp). */ + } +#endif + + /* call the recursion processor */ + err = start_recursion (update_fileproc, update_filesdone_proc, + update_dirent_proc, update_dirleave_proc, NULL, + argc, argv, local, which, aflag, 1, + preload_update_dir, 1); + + /* see if we need to sleep before returning to avoid time-stamp races */ + if (last_register_time) + { + while (time ((time_t *) NULL) == last_register_time) + sleep (1); + } + + return (err); +} + +#ifdef PRESERVE_PERMISSIONS_SUPPORT +/* + * The get_linkinfo_proc callback adds each file to the hardlist + * (see hardlink.c). + */ + +static int +get_linkinfo_proc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *fullpath; + Node *linkp; + struct hardlink_info *hlinfo; + + /* Get the full pathname of the current file. */ + fullpath = xmalloc (strlen(working_dir) + + strlen(finfo->fullname) + 2); + sprintf (fullpath, "%s/%s", working_dir, finfo->fullname); + + /* To permit recursing into subdirectories, files + are keyed on the full pathname and not on the basename. */ + linkp = lookup_file_by_inode (fullpath); + if (linkp == NULL) + { + /* The file isn't on disk; we are probably restoring + a file that was removed. */ + return 0; + } + + /* Create a new, empty hardlink_info node. */ + hlinfo = (struct hardlink_info *) + xmalloc (sizeof (struct hardlink_info)); + + hlinfo->status = (Ctype) 0; /* is this dumb? */ + hlinfo->checked_out = 0; + + linkp->data = (char *) hlinfo; + + return 0; +} +#endif + +/* + * This is the callback proc for update. It is called for each file in each + * directory by the recursion code. The current directory is the local + * instantiation. file is the file name we are to operate on. update_dir is + * set to the path relative to where we started (for pretty printing). + * repository is the repository. entries and srcfiles are the pre-parsed + * entries and source control files. + * + * This routine decides what needs to be done for each file and does the + * appropriate magic for checkout + */ +static int +update_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + int retval; + Ctype status; + Vers_TS *vers; + int resurrecting; + + resurrecting = 0; + + status = Classify_File (finfo, tag, date, options, force_tag_match, + aflag, &vers, pipeout); + + /* Keep track of whether TAG is a branch tag. + Note that if it is a branch tag in some files and a nonbranch tag + in others, treat it as a nonbranch tag. It is possible that case + should elicit a warning or an error. */ + if (rewrite_tag + && tag != NULL + && finfo->rcs != NULL) + { + char *rev = RCS_getversion (finfo->rcs, tag, NULL, 1, NULL); + if (rev != NULL + && !RCS_nodeisbranch (finfo->rcs, tag)) + nonbranch = 1; + if (rev != NULL) + free (rev); + } + + if (pipeout) + { + /* + * We just return success without doing anything if any of the really + * funky cases occur + * + * If there is still a valid RCS file, do a regular checkout type + * operation + */ + switch (status) + { + case T_UNKNOWN: /* unknown file was explicitly asked + * about */ + case T_REMOVE_ENTRY: /* needs to be un-registered */ + case T_ADDED: /* added but not committed */ + retval = 0; + break; + case T_CONFLICT: /* old punt-type errors */ + retval = 1; + break; + case T_UPTODATE: /* file was already up-to-date */ + case T_NEEDS_MERGE: /* needs merging */ + case T_MODIFIED: /* locally modified */ + case T_REMOVED: /* removed but not committed */ + case T_CHECKOUT: /* needs checkout */ +#ifdef SERVER_SUPPORT + case T_PATCH: /* needs patch */ +#endif + retval = checkout_file (finfo, vers, 0, 0, 0); + break; + + default: /* can't ever happen :-) */ + error (0, 0, + "unknown file status %d for file %s", status, finfo->file); + retval = 0; + break; + } + } + else + { + switch (status) + { + case T_UNKNOWN: /* unknown file was explicitly asked + * about */ + case T_UPTODATE: /* file was already up-to-date */ + retval = 0; + break; + case T_CONFLICT: /* old punt-type errors */ + retval = 1; + write_letter (finfo, 'C'); + break; + case T_NEEDS_MERGE: /* needs merging */ + retval = merge_file (finfo, vers); + break; + case T_MODIFIED: /* locally modified */ + retval = 0; + if (toss_local_changes) + { + char *bakname; + bakname = backup_file (finfo->file, vers->vn_user); + /* This behavior is sufficiently unexpected to + justify overinformativeness, I think. */ +#ifdef SERVER_SUPPORT + if ((! really_quiet) && (! server_active)) +#else /* ! SERVER_SUPPORT */ + if (! really_quiet) +#endif /* SERVER_SUPPORT */ + (void) printf ("(Locally modified %s moved to %s)\n", + finfo->file, bakname); + free (bakname); + + /* The locally modified file is still present, but + it will be overwritten by the repository copy + after this. */ + status = T_CHECKOUT; + retval = checkout_file (finfo, vers, 0, 0, 1); + } + else + { + if (vers->ts_conflict) + { + char *filestamp; + int retcode; + + /* + * If the timestamp has changed and no + * conflict indicators are found, it isn't a + * 'C' any more. + */ + +#ifdef SERVER_SUPPORT + if (server_active) + retcode = vers->ts_conflict[0] != '='; + else + { + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); + } +#else + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); +#endif + + if (retcode) + { + /* The timestamps differ. But if there + are conflict markers print 'C' anyway. */ + retcode = !file_has_markers (finfo); + } + + if (!retcode) + { + write_letter (finfo, 'C'); + retval = 1; + } + else + { + /* Reregister to clear conflict flag. */ + Register (finfo->entries, finfo->file, + vers->vn_rcs, vers->ts_rcs, + vers->options, vers->tag, + vers->date, (char *)0); + } + } + if (!retval) + { + write_letter (finfo, 'M'); + retval = 0; + } + } + break; +#ifdef SERVER_SUPPORT + case T_PATCH: /* needs patch */ + if (patches) + { + int docheckout; + struct stat file_info; + unsigned char checksum[16]; + + retval = patch_file (finfo, + vers, &docheckout, + &file_info, checksum); + if (! docheckout) + { + if (server_active && retval == 0) + server_updated (finfo, vers, + (rcs_diff_patches + ? SERVER_RCS_DIFF + : SERVER_PATCHED), + file_info.st_mode, checksum, + (struct buffer *) NULL); + break; + } + } + /* If we're not running as a server, just check the + file out. It's simpler and faster than producing + and applying patches. */ + /* Fall through. */ +#endif + case T_CHECKOUT: /* needs checkout */ + retval = checkout_file (finfo, vers, 0, 0, 1); + break; + case T_ADDED: /* added but not committed */ + write_letter (finfo, 'A'); + retval = 0; + break; + case T_REMOVED: /* removed but not committed */ + write_letter (finfo, 'R'); + retval = 0; + break; + case T_REMOVE_ENTRY: /* needs to be un-registered */ + retval = scratch_file (finfo); +#ifdef SERVER_SUPPORT + if (server_active && retval == 0) + { + if (vers->ts_user == NULL) + server_scratch_entry_only (); + server_updated (finfo, vers, + SERVER_UPDATED, (mode_t) -1, + (unsigned char *) NULL, + (struct buffer *) NULL); + } +#endif + break; + default: /* can't ever happen :-) */ + error (0, 0, + "unknown file status %d for file %s", status, finfo->file); + retval = 0; + break; + } + } + + /* only try to join if things have gone well thus far */ + if (retval == 0 && join_rev1) + join_file (finfo, vers); + + /* if this directory has an ignore list, add this file to it */ + if (ignlist) + { + Node *p; + + p = getnode (); + p->type = FILES; + p->key = xstrdup (finfo->file); + if (addnode (ignlist, p) != 0) + freenode (p); + } + + freevers_ts (&vers); + return (retval); +} + +static void update_ignproc PROTO ((char *, char *)); + +static void +update_ignproc (file, dir) + char *file; + char *dir; +{ + struct file_info finfo; + + memset (&finfo, 0, sizeof (finfo)); + finfo.file = file; + finfo.update_dir = dir; + if (dir[0] == '\0') + finfo.fullname = xstrdup (file); + else + { + finfo.fullname = xmalloc (strlen (file) + strlen (dir) + 10); + strcpy (finfo.fullname, dir); + strcat (finfo.fullname, "/"); + strcat (finfo.fullname, file); + } + + write_letter (&finfo, '?'); + free (finfo.fullname); +} + +/* ARGSUSED */ +static int +update_filesdone_proc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + char *repository; + char *update_dir; + List *entries; +{ + if (rewrite_tag) + { + WriteTag (NULL, tag, date, nonbranch, update_dir, repository); + rewrite_tag = 0; + } + + /* if this directory has an ignore list, process it then free it */ + if (ignlist) + { + ignore_files (ignlist, entries, update_dir, update_ignproc); + dellist (&ignlist); + } + + /* Clean up CVS admin dirs if we are export */ + if (strcmp (command_name, "export") == 0) + { + /* I'm not sure the existence_error is actually possible (except + in cases where we really should print a message), but since + this code used to ignore all errors, I'll play it safe. */ + if (unlink_file_dir (CVSADM) < 0 && !existence_error (errno)) + error (0, errno, "cannot remove %s directory", CVSADM); + } +#ifdef SERVER_SUPPORT + else if (!server_active && !pipeout) +#else + else if (!pipeout) +#endif /* SERVER_SUPPORT */ + { + /* If there is no CVS/Root file, add one */ + if (!isfile (CVSADM_ROOT)) + Create_Root ((char *) NULL, CVSroot_original); + } + + return (err); +} + +/* + * update_dirent_proc () is called back by the recursion processor before a + * sub-directory is processed for update. In this case, update_dirent proc + * will probably create the directory unless -d isn't specified and this is a + * new directory. A return code of 0 indicates the directory should be + * processed by the recursion code. A return of non-zero indicates the + * recursion code should skip this directory. + */ +static Dtype +update_dirent_proc (callerdat, dir, repository, update_dir, entries) + void *callerdat; + char *dir; + char *repository; + char *update_dir; + List *entries; +{ + if (ignore_directory (update_dir)) + { + /* print the warm fuzzy message */ + if (!quiet) + error (0, 0, "Ignoring %s", update_dir); + return R_SKIP_ALL; + } + + if (!isdir (dir)) + { + /* if we aren't building dirs, blow it off */ + if (!update_build_dirs) + return (R_SKIP_ALL); + + /* Various CVS administrators are in the habit of removing + the repository directory for things they don't want any + more. I've even been known to do it myself (on rare + occasions). Not the usual recommended practice, but we + want to try to come up with some kind of + reasonable/documented/sensible behavior. Generally + the behavior is to just skip over that directory (see + dirs test in sanity.sh; the case which reaches here + is when update -d is specified, and the working directory + is gone but the subdirectory is still mentioned in + CVS/Entries). */ + if (1 +#ifdef SERVER_SUPPORT + /* In the remote case, the client should refrain from + sending us the directory in the first place. So we + want to continue to give an error, so clients make + sure to do this. */ + && !server_active +#endif + && !isdir (repository)) + return R_SKIP_ALL; + + if (noexec) + { + /* ignore the missing dir if -n is specified */ + error (0, 0, "New directory `%s' -- ignored", update_dir); + return (R_SKIP_ALL); + } + else + { + /* otherwise, create the dir and appropriate adm files */ + + /* If no tag or date were specified on the command line, + and we're not using -A, we want the subdirectory to use + the tag and date, if any, of the current directory. + That way, update -d will work correctly when working on + a branch. + + We use TAG_UPDATE_DIR to undo the tag setting in + update_dirleave_proc. If we did not do this, we would + not correctly handle a working directory with multiple + tags (and maybe we should prohibit such working + directories, but they work now and we shouldn't make + them stop working without more thought). */ + if ((tag == NULL && date == NULL) && ! aflag) + { + ParseTag (&tag, &date, &nonbranch); + if (tag != NULL || date != NULL) + tag_update_dir = xstrdup (update_dir); + } + + make_directory (dir); + Create_Admin (dir, update_dir, repository, tag, date, + /* This is a guess. We will rewrite it later + via WriteTag. */ + 0, + 0); + rewrite_tag = 1; + nonbranch = 0; + Subdir_Register (entries, (char *) NULL, dir); + } + } + /* Do we need to check noexec here? */ + else if (!pipeout) + { + char *cvsadmdir; + + /* The directory exists. Check to see if it has a CVS + subdirectory. */ + + cvsadmdir = xmalloc (strlen (dir) + 80); + strcpy (cvsadmdir, dir); + strcat (cvsadmdir, "/"); + strcat (cvsadmdir, CVSADM); + + if (!isdir (cvsadmdir)) + { + /* We cannot successfully recurse into a directory without a CVS + subdirectory. Generally we will have already printed + "? foo". */ + free (cvsadmdir); + return R_SKIP_ALL; + } + free (cvsadmdir); + } + + /* + * If we are building dirs and not going to stdout, we make sure there is + * no static entries file and write the tag file as appropriate + */ + if (!pipeout) + { + if (update_build_dirs) + { + char *tmp; + + tmp = xmalloc (strlen (dir) + sizeof (CVSADM_ENTSTAT) + 10); + (void) sprintf (tmp, "%s/%s", dir, CVSADM_ENTSTAT); + if (unlink_file (tmp) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove file %s", tmp); +#ifdef SERVER_SUPPORT + if (server_active) + server_clear_entstat (update_dir, repository); +#endif + free (tmp); + } + + /* keep the CVS/Tag file current with the specified arguments */ + if (aflag || tag || date) + { + WriteTag (dir, tag, date, 0, update_dir, repository); + rewrite_tag = 1; + nonbranch = 0; + } + + /* initialize the ignore list for this directory */ + ignlist = getlist (); + } + + /* print the warm fuzzy message */ + if (!quiet) + error (0, 0, "Updating %s", update_dir); + + return (R_PROCESS); +} + +/* + * update_dirleave_proc () is called back by the recursion code upon leaving + * a directory. It will prune empty directories if needed and will execute + * any appropriate update programs. + */ +/* ARGSUSED */ +static int +update_dirleave_proc (callerdat, dir, err, update_dir, entries) + void *callerdat; + char *dir; + int err; + char *update_dir; + List *entries; +{ + FILE *fp; + + /* If we set the tag or date for a new subdirectory in + update_dirent_proc, and we're now done with that subdirectory, + undo the tag/date setting. Note that we know that the tag and + date were both originally NULL in this case. */ + if (tag_update_dir != NULL && strcmp (update_dir, tag_update_dir) == 0) + { + if (tag != NULL) + { + free (tag); + tag = NULL; + } + if (date != NULL) + { + free (date); + date = NULL; + } + nonbranch = 0; + free (tag_update_dir); + tag_update_dir = NULL; + } + + /* run the update_prog if there is one */ + /* FIXME: should be checking for errors from CVS_FOPEN and printing + them if not existence_error. */ + if (err == 0 && !pipeout && !noexec && + (fp = CVS_FOPEN (CVSADM_UPROG, "r")) != NULL) + { + char *cp; + char *repository; + char *line = NULL; + size_t line_allocated = 0; + + repository = Name_Repository ((char *) NULL, update_dir); + if (getline (&line, &line_allocated, fp) >= 0) + { + if ((cp = strrchr (line, '\n')) != NULL) + *cp = '\0'; + run_setup (line); + run_arg (repository); + cvs_output (program_name, 0); + cvs_output (" ", 1); + cvs_output (command_name, 0); + cvs_output (": Executing '", 0); + run_print (stdout); + cvs_output ("'\n", 0); + (void) run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL); + } + else if (ferror (fp)) + error (0, errno, "cannot read %s", CVSADM_UPROG); + else + error (0, 0, "unexpected end of file on %s", CVSADM_UPROG); + + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", CVSADM_UPROG); + if (line != NULL) + free (line); + free (repository); + } + + if (strchr (dir, '/') == NULL) + { + /* FIXME: chdir ("..") loses with symlinks. */ + /* Prune empty dirs on the way out - if necessary */ + (void) CVS_CHDIR (".."); + if (update_prune_dirs && isemptydir (dir, 0)) + { + /* I'm not sure the existence_error is actually possible (except + in cases where we really should print a message), but since + this code used to ignore all errors, I'll play it safe. */ + if (unlink_file_dir (dir) < 0 && !existence_error (errno)) + error (0, errno, "cannot remove %s directory", dir); + Subdir_Deregister (entries, (char *) NULL, dir); + } + } + + return (err); +} + +static int isremoved PROTO ((Node *, void *)); + +/* Returns 1 if the file indicated by node has been removed. */ +static int +isremoved (node, closure) + Node *node; + void *closure; +{ + Entnode *entdata = (Entnode*) node->data; + + /* If the first character of the version is a '-', the file has been + removed. */ + return (entdata->version && entdata->version[0] == '-') ? 1 : 0; +} + +/* Returns 1 if the argument directory is completely empty, other than the + existence of the CVS directory entry. Zero otherwise. If MIGHT_NOT_EXIST + and the directory doesn't exist, then just return 0. */ +int +isemptydir (dir, might_not_exist) + char *dir; + int might_not_exist; +{ + DIR *dirp; + struct dirent *dp; + + if ((dirp = CVS_OPENDIR (dir)) == NULL) + { + if (might_not_exist && existence_error (errno)) + return 0; + error (0, errno, "cannot open directory %s for empty check", dir); + return (0); + } + errno = 0; + while ((dp = readdir (dirp)) != NULL) + { + if (strcmp (dp->d_name, ".") != 0 + && strcmp (dp->d_name, "..") != 0) + { + if (strcmp (dp->d_name, CVSADM) != 0) + { + /* An entry other than the CVS directory. The directory + is certainly not empty. */ + (void) closedir (dirp); + return (0); + } + else + { + /* The CVS directory entry. We don't have to worry about + this unless the Entries file indicates that files have + been removed, but not committed, in this directory. + (Removing the directory would prevent people from + comitting the fact that they removed the files!) */ + List *l; + int files_removed; + struct saved_cwd cwd; + + if (save_cwd (&cwd)) + error_exit (); + + if (CVS_CHDIR (dir) < 0) + error (1, errno, "cannot change directory to %s", dir); + l = Entries_Open (0, NULL); + files_removed = walklist (l, isremoved, 0); + Entries_Close (l); + + if (restore_cwd (&cwd, NULL)) + error_exit (); + free_cwd (&cwd); + + if (files_removed != 0) + { + /* There are files that have been removed, but not + committed! Do not consider the directory empty. */ + (void) closedir (dirp); + return (0); + } + } + } + errno = 0; + } + if (errno != 0) + { + error (0, errno, "cannot read directory %s", dir); + (void) closedir (dirp); + return (0); + } + (void) closedir (dirp); + return (1); +} + +/* + * scratch the Entries file entry associated with a file + */ +static int +scratch_file (finfo) + struct file_info *finfo; +{ + history_write ('W', finfo->update_dir, "", finfo->file, finfo->repository); + Scratch_Entry (finfo->entries, finfo->file); + if (unlink_file (finfo->file) < 0 && ! existence_error (errno)) + error (0, errno, "unable to remove %s", finfo->fullname); + return (0); +} + +/* + * Check out a file. + */ +static int +checkout_file (finfo, vers_ts, adding, merging, update_server) + struct file_info *finfo; + Vers_TS *vers_ts; + int adding; + int merging; + int update_server; +{ + char *backup; + int set_time, retval = 0; + int status; + int file_is_dead; + struct buffer *revbuf; + + backup = NULL; + revbuf = NULL; + + /* Don't screw with backup files if we're going to stdout, or if + we are the server. */ + if (!pipeout +#ifdef SERVER_SUPPORT + && ! server_active +#endif + ) + { + backup = xmalloc (strlen (finfo->file) + + sizeof (CVSADM) + + sizeof (CVSPREFIX) + + 10); + (void) sprintf (backup, "%s/%s%s", CVSADM, CVSPREFIX, finfo->file); + if (isfile (finfo->file)) + rename_file (finfo->file, backup); + else + { + /* If -f/-t wrappers are being used to wrap up a directory, + then backup might be a directory instead of just a file. */ + if (unlink_file_dir (backup) < 0) + { + /* Not sure if the existence_error check is needed here. */ + if (!existence_error (errno)) + /* FIXME: should include update_dir in message. */ + error (0, errno, "error removing %s", backup); + } + free (backup); + backup = NULL; + } + } + + file_is_dead = RCS_isdead (vers_ts->srcfile, vers_ts->vn_rcs); + + if (!file_is_dead) + { + /* + * if we are checking out to stdout, print a nice message to + * stderr, and add the -p flag to the command */ + if (pipeout) + { + if (!quiet) + { + cvs_outerr ("\ +===================================================================\n\ +Checking out ", 0); + cvs_outerr (finfo->fullname, 0); + cvs_outerr ("\n\ +RCS: ", 0); + cvs_outerr (vers_ts->srcfile->path, 0); + cvs_outerr ("\n\ +VERS: ", 0); + cvs_outerr (vers_ts->vn_rcs, 0); + cvs_outerr ("\n***************\n", 0); + } + } + +#ifdef SERVER_SUPPORT + if (update_server + && server_active + && ! pipeout + && ! file_gzip_level + && ! joining () + && ! wrap_name_has (finfo->file, WRAP_FROMCVS)) + { + revbuf = buf_nonio_initialize ((BUFMEMERRPROC) NULL); + status = RCS_checkout (vers_ts->srcfile, (char *) NULL, + vers_ts->vn_rcs, vers_ts->vn_tag, + vers_ts->options, RUN_TTY, + checkout_to_buffer, revbuf); + } + else +#endif + status = RCS_checkout (vers_ts->srcfile, + pipeout ? NULL : finfo->file, + vers_ts->vn_rcs, vers_ts->vn_tag, + vers_ts->options, RUN_TTY, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + } + if (file_is_dead || status == 0) + { + mode_t mode; + + mode = (mode_t) -1; + + if (!pipeout) + { + Vers_TS *xvers_ts; + + if (revbuf != NULL && !noexec) + { + struct stat sb; + + /* FIXME: We should have RCS_checkout return the mode. + That would also fix the kludge with noexec, above, which + is here only because noexec doesn't write srcfile->path + for us to stat. */ + if (stat (vers_ts->srcfile->path, &sb) < 0) + error (1, errno, "cannot stat %s", + vers_ts->srcfile->path); + mode = sb.st_mode &~ (S_IWRITE | S_IWGRP | S_IWOTH); + } + + if (cvswrite + && !file_is_dead + && !fileattr_get (finfo->file, "_watched")) + { + if (revbuf == NULL) + xchmod (finfo->file, 1); + else + { + /* We know that we are the server here, so + although xchmod checks umask, we don't bother. */ + mode |= (((mode & S_IRUSR) ? S_IWUSR : 0) + | ((mode & S_IRGRP) ? S_IWGRP : 0) + | ((mode & S_IROTH) ? S_IWOTH : 0)); + } + } + + { + /* A newly checked out file is never under the spell + of "cvs edit". If we think we were editing it + from a previous life, clean up. Would be better to + check for same the working directory instead of + same user, but that is hairy. */ + + struct addremove_args args; + + editor_set (finfo->file, getcaller (), NULL); + + memset (&args, 0, sizeof args); + args.remove_temp = 1; + watch_modify_watchers (finfo->file, &args); + } + + /* set the time from the RCS file iff it was unknown before */ + set_time = + (!noexec + && (vers_ts->vn_user == NULL || + strncmp (vers_ts->ts_rcs, "Initial", 7) == 0) + && !file_is_dead); + + wrap_fromcvs_process_file (finfo->file); + + xvers_ts = Version_TS (finfo, options, tag, date, + force_tag_match, set_time); + if (strcmp (xvers_ts->options, "-V4") == 0) + xvers_ts->options[0] = '\0'; + + if (revbuf != NULL) + { + /* If we stored the file data into a buffer, then we + didn't create a file at all, so xvers_ts->ts_user + is wrong. The correct value is to have it be the + same as xvers_ts->ts_rcs, meaning that the working + file is unchanged from the RCS file. + + FIXME: We should tell Version_TS not to waste time + statting the nonexistent file. + + FIXME: Actually, I don't think the ts_user value + matters at all here. The only use I know of is + that it is printed in a trace message by + Server_Register. */ + + if (xvers_ts->ts_user != NULL) + free (xvers_ts->ts_user); + xvers_ts->ts_user = xstrdup (xvers_ts->ts_rcs); + } + + (void) time (&last_register_time); + + if (file_is_dead) + { + if (xvers_ts->vn_user != NULL) + { + error (0, 0, + "warning: %s is not (any longer) pertinent", + finfo->fullname); + } + Scratch_Entry (finfo->entries, finfo->file); +#ifdef SERVER_SUPPORT + if (server_active && xvers_ts->ts_user == NULL) + server_scratch_entry_only (); +#endif + /* FIXME: Rather than always unlink'ing, and ignoring the + existence_error, we should do the unlink only if + vers_ts->ts_user is non-NULL. Then there would be no + need to ignore an existence_error (for example, if the + user removes the file while we are running). */ + if (unlink_file (finfo->file) < 0 && ! existence_error (errno)) + { + error (0, errno, "cannot remove %s", finfo->fullname); + } + } + else + Register (finfo->entries, finfo->file, + adding ? "0" : xvers_ts->vn_rcs, + xvers_ts->ts_user, xvers_ts->options, + xvers_ts->tag, xvers_ts->date, + (char *)0); /* Clear conflict flag on fresh checkout */ + + /* fix up the vers structure, in case it is used by join */ + if (join_rev1) + { + if (vers_ts->vn_user != NULL) + free (vers_ts->vn_user); + if (vers_ts->vn_rcs != NULL) + free (vers_ts->vn_rcs); + vers_ts->vn_user = xstrdup (xvers_ts->vn_rcs); + vers_ts->vn_rcs = xstrdup (xvers_ts->vn_rcs); + } + + /* If this is really Update and not Checkout, recode history */ + if (strcmp (command_name, "update") == 0) + history_write ('U', finfo->update_dir, xvers_ts->vn_rcs, finfo->file, + finfo->repository); + + freevers_ts (&xvers_ts); + + if (!really_quiet && !file_is_dead) + { + write_letter (finfo, 'U'); + } + } + +#ifdef SERVER_SUPPORT + if (update_server && server_active) + server_updated (finfo, vers_ts, + merging ? SERVER_MERGED : SERVER_UPDATED, + mode, (unsigned char *) NULL, revbuf); +#endif + } + else + { + if (backup != NULL) + { + rename_file (backup, finfo->file); + free (backup); + backup = NULL; + } + + error (0, 0, "could not check out %s", finfo->fullname); + + retval = status; + } + + if (backup != NULL) + { + /* If -f/-t wrappers are being used to wrap up a directory, + then backup might be a directory instead of just a file. */ + if (unlink_file_dir (backup) < 0) + { + /* Not sure if the existence_error check is needed here. */ + if (!existence_error (errno)) + /* FIXME: should include update_dir in message. */ + error (0, errno, "error removing %s", backup); + } + free (backup); + } + + return (retval); +} + +#ifdef SERVER_SUPPORT + +/* This function is used to write data from a file being checked out + into a buffer. */ + +static void +checkout_to_buffer (callerdat, data, len) + void *callerdat; + const char *data; + size_t len; +{ + struct buffer *buf = (struct buffer *) callerdat; + + buf_output (buf, data, len); +} + +#endif /* SERVER_SUPPORT */ + +#ifdef SERVER_SUPPORT + +/* This structure is used to pass information between patch_file and + patch_file_write. */ + +struct patch_file_data +{ + /* File name, for error messages. */ + const char *filename; + /* File to which to write. */ + FILE *fp; + /* Whether to compute the MD5 checksum. */ + int compute_checksum; + /* Data structure for computing the MD5 checksum. */ + struct cvs_MD5Context context; + /* Set if the file has a final newline. */ + int final_nl; +}; + +/* Patch a file. Runs diff. This is only done when running as the + * server. The hope is that the diff will be smaller than the file + * itself. + */ +static int +patch_file (finfo, vers_ts, docheckout, file_info, checksum) + struct file_info *finfo; + Vers_TS *vers_ts; + int *docheckout; + struct stat *file_info; + unsigned char *checksum; +{ + char *backup; + char *file1; + char *file2; + int retval = 0; + int retcode = 0; + int fail; + FILE *e; + struct patch_file_data data; + + *docheckout = 0; + + if (noexec || pipeout || joining ()) + { + *docheckout = 1; + return 0; + } + + /* If this file has been marked as being binary, then never send a + patch. */ + if (strcmp (vers_ts->options, "-kb") == 0) + { + *docheckout = 1; + return 0; + } + + /* First check that the first revision exists. If it has been nuked + by cvs admin -o, then just fall back to checking out entire + revisions. In some sense maybe we don't have to do this; after + all cvs.texinfo says "Make sure that no-one has checked out a + copy of the revision you outdate" but then again, that advice + doesn't really make complete sense, because "cvs admin" operates + on a working directory and so _someone_ will almost always have + _some_ revision checked out. */ + { + char *rev; + + rev = RCS_gettag (finfo->rcs, vers_ts->vn_user, 1, NULL); + if (rev == NULL) + { + *docheckout = 1; + return 0; + } + else + free (rev); + } + + /* If the revision is dead, let checkout_file handle it rather + than duplicating the processing here. */ + if (RCS_isdead (vers_ts->srcfile, vers_ts->vn_rcs)) + { + *docheckout = 1; + return 0; + } + + backup = xmalloc (strlen (finfo->file) + + sizeof (CVSADM) + + sizeof (CVSPREFIX) + + 10); + (void) sprintf (backup, "%s/%s%s", CVSADM, CVSPREFIX, finfo->file); + if (isfile (finfo->file)) + rename_file (finfo->file, backup); + else + { + if (unlink_file (backup) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", backup); + } + + file1 = xmalloc (strlen (finfo->file) + + sizeof (CVSADM) + + sizeof (CVSPREFIX) + + 10); + (void) sprintf (file1, "%s/%s%s-1", CVSADM, CVSPREFIX, finfo->file); + file2 = xmalloc (strlen (finfo->file) + + sizeof (CVSADM) + + sizeof (CVSPREFIX) + + 10); + (void) sprintf (file2, "%s/%s%s-2", CVSADM, CVSPREFIX, finfo->file); + + fail = 0; + + /* We need to check out both revisions first, to see if either one + has a trailing newline. Because of this, we don't use rcsdiff, + but just use diff. */ + + e = CVS_FOPEN (file1, "w"); + if (e == NULL) + error (1, errno, "cannot open %s", file1); + + data.filename = file1; + data.fp = e; + data.final_nl = 0; + data.compute_checksum = 0; + + retcode = RCS_checkout (vers_ts->srcfile, (char *) NULL, + vers_ts->vn_user, (char *) NULL, + vers_ts->options, RUN_TTY, + patch_file_write, (void *) &data); + + if (fclose (e) < 0) + error (1, errno, "cannot close %s", file1); + + if (retcode != 0 || ! data.final_nl) + fail = 1; + + if (! fail) + { + e = CVS_FOPEN (file2, "w"); + if (e == NULL) + error (1, errno, "cannot open %s", file2); + + data.filename = file2; + data.fp = e; + data.final_nl = 0; + data.compute_checksum = 1; + cvs_MD5Init (&data.context); + + retcode = RCS_checkout (vers_ts->srcfile, (char *) NULL, + vers_ts->vn_rcs, vers_ts->vn_tag, + vers_ts->options, RUN_TTY, + patch_file_write, (void *) &data); + + if (fclose (e) < 0) + error (1, errno, "cannot close %s", file2); + + if (retcode != 0 || ! data.final_nl) + fail = 1; + else + cvs_MD5Final (checksum, &data.context); + } + + retcode = 0; + if (! fail) + { + char *diff_options; + + /* If the client does not support the Rcs-diff command, we + send a context diff, and the client must invoke patch. + That approach was problematical for various reasons. The + new approach only requires running diff in the server; the + client can handle everything without invoking an external + program. */ + if (! rcs_diff_patches) + { + /* We use -c, not -u, because that is what CVS has + traditionally used. Kind of a moot point, now that + Rcs-diff is preferred, so there is no point in making + the compatibility issues worse. */ + diff_options = "-c"; + } + else + { + /* Now that diff is librarified, we could be passing -a if + we wanted to. However, it is unclear to me whether we + would want to. Does diff -a, in any significant + percentage of cases, produce patches which are smaller + than the files it is patching? I guess maybe text + files with character sets which diff regards as + 'binary'. Conversely, do they tend to be much larger + in the bad cases? This needs some more + thought/investigation, I suspect. */ + + diff_options = "-n"; + } + retcode = diff_exec (file1, file2, diff_options, finfo->file); + + /* A retcode of 0 means no differences. 1 means some differences. */ + if (retcode != 0 + && retcode != 1) + { + fail = 1; + } + else + { +#define BINARY "Binary" + char buf[sizeof BINARY]; + unsigned int c; + + /* Stat the original RCS file, and then adjust it the way + that RCS_checkout would. FIXME: This is an abstraction + violation. */ + if (CVS_STAT (vers_ts->srcfile->path, file_info) < 0) + error (1, errno, "could not stat %s", vers_ts->srcfile->path); + if (chmod (finfo->file, + file_info->st_mode & ~(S_IWRITE | S_IWGRP | S_IWOTH)) + < 0) + error (0, errno, "cannot change mode of file %s", finfo->file); + if (cvswrite + && !fileattr_get (finfo->file, "_watched")) + xchmod (finfo->file, 1); + + /* Check the diff output to make sure patch will be handle it. */ + e = CVS_FOPEN (finfo->file, "r"); + if (e == NULL) + error (1, errno, "could not open diff output file %s", + finfo->fullname); + c = fread (buf, 1, sizeof BINARY - 1, e); + buf[c] = '\0'; + if (strcmp (buf, BINARY) == 0) + { + /* These are binary files. We could use diff -a, but + patch can't handle that. */ + fail = 1; + } + fclose (e); + } + } + + if (! fail) + { + Vers_TS *xvers_ts; + + /* This stuff is just copied blindly from checkout_file. I + don't really know what it does. */ + xvers_ts = Version_TS (finfo, options, tag, date, + force_tag_match, 0); + if (strcmp (xvers_ts->options, "-V4") == 0) + xvers_ts->options[0] = '\0'; + + Register (finfo->entries, finfo->file, xvers_ts->vn_rcs, + xvers_ts->ts_user, xvers_ts->options, + xvers_ts->tag, xvers_ts->date, NULL); + + if (CVS_STAT (finfo->file, file_info) < 0) + error (1, errno, "could not stat %s", finfo->file); + + /* If this is really Update and not Checkout, recode history */ + if (strcmp (command_name, "update") == 0) + history_write ('P', finfo->update_dir, xvers_ts->vn_rcs, finfo->file, + finfo->repository); + + freevers_ts (&xvers_ts); + + if (!really_quiet) + { + write_letter (finfo, 'P'); + } + } + else + { + int old_errno = errno; /* save errno value over the rename */ + + if (isfile (backup)) + rename_file (backup, finfo->file); + + if (retcode != 0 && retcode != 1) + error (retcode == -1 ? 1 : 0, retcode == -1 ? old_errno : 0, + "could not diff %s", finfo->fullname); + + *docheckout = 1; + retval = retcode; + } + + if (unlink_file (backup) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", backup); + if (unlink_file (file1) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", file1); + if (unlink_file (file2) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", file2); + + free (backup); + free (file1); + free (file2); + return (retval); +} + +/* Write data to a file. Record whether the last byte written was a + newline. Optionally compute a checksum. This is called by + patch_file via RCS_checkout. */ + +static void +patch_file_write (callerdat, buffer, len) + void *callerdat; + const char *buffer; + size_t len; +{ + struct patch_file_data *data = (struct patch_file_data *) callerdat; + + if (fwrite (buffer, 1, len, data->fp) != len) + error (1, errno, "cannot write %s", data->filename); + + data->final_nl = (buffer[len - 1] == '\n'); + + if (data->compute_checksum) + cvs_MD5Update (&data->context, (unsigned char *) buffer, len); +} + +#endif /* SERVER_SUPPORT */ + +/* + * Several of the types we process only print a bit of information consisting + * of a single letter and the name. + */ +static void +write_letter (finfo, letter) + struct file_info *finfo; + int letter; +{ + if (!really_quiet) + { + char *tag = NULL; + /* Big enough for "+updated" or any of its ilk. */ + char buf[80]; + + switch (letter) + { + case 'U': + tag = "updated"; + break; + default: + /* We don't yet support tagged output except for "U". */ + break; + } + + if (tag != NULL) + { + sprintf (buf, "+%s", tag); + cvs_output_tagged (buf, NULL); + } + buf[0] = letter; + buf[1] = ' '; + buf[2] = '\0'; + cvs_output_tagged ("text", buf); + cvs_output_tagged ("fname", finfo->fullname); + cvs_output_tagged ("newline", NULL); + if (tag != NULL) + { + sprintf (buf, "-%s", tag); + cvs_output_tagged (buf, NULL); + } + } + return; +} + +/* + * Do all the magic associated with a file which needs to be merged + */ +static int +merge_file (finfo, vers) + struct file_info *finfo; + Vers_TS *vers; +{ + char *backup; + int status; + int retcode = 0; + int retval; + + /* + * The users currently modified file is moved to a backup file name + * ".#filename.version", so that it will stay around for a few days + * before being automatically removed by some cron daemon. The "version" + * is the version of the file that the user was most up-to-date with + * before the merge. + */ + backup = xmalloc (strlen (finfo->file) + + strlen (vers->vn_user) + + sizeof (BAKPREFIX) + + 10); + (void) sprintf (backup, "%s%s.%s", BAKPREFIX, finfo->file, vers->vn_user); + + if (unlink_file (backup) && !existence_error (errno)) + error (0, errno, "unable to remove %s", backup); + copy_file (finfo->file, backup); + xchmod (finfo->file, 1); + + if (strcmp (vers->options, "-kb") == 0 + || wrap_merge_is_copy (finfo->file) + || special_file_mismatch (finfo, NULL, vers->vn_rcs)) + { + /* For binary files, a merge is always a conflict. Same for + files whose permissions or linkage do not match. We give the + user the two files, and let them resolve it. It is possible + that we should require a "touch foo" or similar step before + we allow a checkin. */ + + /* TODO: it may not always be necessary to regard a permission + mismatch as a conflict. The working file and the RCS file + have a common ancestor `A'; if the working file's permissions + match A's, then it's probably safe to overwrite them with the + RCS permissions. Only if the working file, the RCS file, and + A all disagree should this be considered a conflict. But more + thought needs to go into this, and in the meantime it is safe + to treat any such mismatch as an automatic conflict. -twp */ + +#ifdef SERVER_SUPPORT + if (server_active) + server_copy_file (finfo->file, finfo->update_dir, + finfo->repository, backup); +#endif + + status = checkout_file (finfo, vers, 0, 1, 1); + + /* Is there a better term than "nonmergeable file"? What we + really mean is, not something that CVS cannot or does not + want to merge (there might be an external manual or + automatic merge process). */ + error (0, 0, "nonmergeable file needs merge"); + error (0, 0, "revision %s from repository is now in %s", + vers->vn_rcs, finfo->fullname); + error (0, 0, "file from working directory is now in %s", backup); + write_letter (finfo, 'C'); + + history_write ('C', finfo->update_dir, vers->vn_rcs, finfo->file, + finfo->repository); + retval = 0; + goto out; + } + + status = RCS_merge(finfo->rcs, vers->srcfile->path, finfo->file, + vers->options, vers->vn_user, vers->vn_rcs); + if (status != 0 && status != 1) + { + error (0, status == -1 ? errno : 0, + "could not merge revision %s of %s", vers->vn_user, finfo->fullname); + error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s", + finfo->fullname, backup); + rename_file (backup, finfo->file); + retval = 1; + goto out; + } + + if (strcmp (vers->options, "-V4") == 0) + vers->options[0] = '\0'; + + /* This file is the result of a merge, which means that it has + been modified. We use a special timestamp string which will + not compare equal to any actual timestamp. */ + { + char *cp = 0; + + if (status) + { + (void) time (&last_register_time); + cp = time_stamp (finfo->file); + } + Register (finfo->entries, finfo->file, vers->vn_rcs, + "Result of merge", vers->options, vers->tag, + vers->date, cp); + if (cp) + free (cp); + } + + /* fix up the vers structure, in case it is used by join */ + if (join_rev1) + { + if (vers->vn_user != NULL) + free (vers->vn_user); + vers->vn_user = xstrdup (vers->vn_rcs); + } + +#ifdef SERVER_SUPPORT + /* Send the new contents of the file before the message. If we + wanted to be totally correct, we would have the client write + the message only after the file has safely been written. */ + if (server_active) + { + server_copy_file (finfo->file, finfo->update_dir, finfo->repository, + backup); + server_updated (finfo, vers, SERVER_MERGED, + (mode_t) -1, (unsigned char *) NULL, + (struct buffer *) NULL); + } +#endif + + /* FIXME: the noexec case is broken. RCS_merge could be doing the + xcmp on the temporary files without much hassle, I think. */ + if (!noexec && !xcmp (backup, finfo->file)) + { + cvs_output (finfo->fullname, 0); + cvs_output (" already contains the differences between ", 0); + cvs_output (vers->vn_user, 0); + cvs_output (" and ", 0); + cvs_output (vers->vn_rcs, 0); + cvs_output ("\n", 1); + + history_write ('G', finfo->update_dir, vers->vn_rcs, finfo->file, + finfo->repository); + retval = 0; + goto out; + } + + if (status == 1) + { + error (0, 0, "conflicts found in %s", finfo->fullname); + + write_letter (finfo, 'C'); + + history_write ('C', finfo->update_dir, vers->vn_rcs, finfo->file, finfo->repository); + + } + else if (retcode == -1) + { + error (1, errno, "fork failed while examining update of %s", + finfo->fullname); + } + else + { + write_letter (finfo, 'M'); + history_write ('G', finfo->update_dir, vers->vn_rcs, finfo->file, + finfo->repository); + } + retval = 0; + out: + free (backup); + return retval; +} + +/* + * Do all the magic associated with a file which needs to be joined + * (-j option) + */ +static void +join_file (finfo, vers) + struct file_info *finfo; + Vers_TS *vers; +{ + char *backup; + char *options; + int status; + + char *rev1; + char *rev2; + char *jrev1; + char *jrev2; + char *jdate1; + char *jdate2; + + jrev1 = join_rev1; + jrev2 = join_rev2; + jdate1 = date_rev1; + jdate2 = date_rev2; + + /* Determine if we need to do anything at all. */ + if (vers->srcfile == NULL || + vers->srcfile->path == NULL) + { + return; + } + + /* If only one join revision is specified, it becomes the second + revision. */ + if (jrev2 == NULL) + { + jrev2 = jrev1; + jrev1 = NULL; + jdate2 = jdate1; + jdate1 = NULL; + } + + /* Convert the second revision, walking branches and dates. */ + rev2 = RCS_getversion (vers->srcfile, jrev2, jdate2, 1, (int *) NULL); + + /* If this is a merge of two revisions, get the first revision. + If only one join tag was specified, then the first revision is + the greatest common ancestor of the second revision and the + working file. */ + if (jrev1 != NULL) + rev1 = RCS_getversion (vers->srcfile, jrev1, jdate1, 1, (int *) NULL); + else + { + /* Note that we use vn_rcs here, since vn_user may contain a + special string such as "-nn". */ + if (vers->vn_rcs == NULL) + rev1 = NULL; + else if (rev2 == NULL) + { + /* This means that the file never existed on the branch. + It does not mean that the file was removed on the + branch: that case is represented by a dead rev2. If + the file never existed on the branch, then we have + nothing to merge, so we just return. */ + return; + } + else + rev1 = gca (vers->vn_rcs, rev2); + } + + /* Handle a nonexistent or dead merge target. */ + if (rev2 == NULL || RCS_isdead (vers->srcfile, rev2)) + { + char *mrev; + + if (rev2 != NULL) + free (rev2); + + /* If the first revision doesn't exist either, then there is + no change between the two revisions, so we don't do + anything. */ + if (rev1 == NULL || RCS_isdead (vers->srcfile, rev1)) + { + if (rev1 != NULL) + free (rev1); + return; + } + + /* If we are merging two revisions, then the file was removed + between the first revision and the second one. In this + case we want to mark the file for removal. + + If we are merging one revision, then the file has been + removed between the greatest common ancestor and the merge + revision. From the perspective of the branch on to which + we ar emerging, which may be the trunk, either 1) the file + does not currently exist on the target, or 2) the file has + not been modified on the target branch since the greatest + common ancestor, or 3) the file has been modified on the + target branch since the greatest common ancestor. In case + 1 there is nothing to do. In case 2 we mark the file for + removal. In case 3 we have a conflict. + + Note that the handling is slightly different depending upon + whether one or two join targets were specified. If two + join targets were specified, we don't check whether the + file was modified since a given point. My reasoning is + that if you ask for an explicit merge between two tags, + then you want to merge in whatever was changed between + those two tags. If a file was removed between the two + tags, then you want it to be removed. However, if you ask + for a merge of a branch, then you want to merge in all + changes which were made on the branch. If a file was + removed on the branch, that is a change to the file. If + the file was also changed on the main line, then that is + also a change. These two changes--the file removal and the + modification--must be merged. This is a conflict. */ + + /* If the user file is dead, or does not exist, or has been + marked for removal, then there is nothing to do. */ + if (vers->vn_user == NULL + || vers->vn_user[0] == '-' + || RCS_isdead (vers->srcfile, vers->vn_user)) + { + if (rev1 != NULL) + free (rev1); + return; + } + + /* If the user file has been marked for addition, or has been + locally modified, then we have a conflict which we can not + resolve. No_Difference will already have been called in + this case, so comparing the timestamps is sufficient to + determine whether the file is locally modified. */ + if (strcmp (vers->vn_user, "0") == 0 + || (vers->ts_user != NULL + && strcmp (vers->ts_user, vers->ts_rcs) != 0)) + { + if (jdate2 != NULL) + error (0, 0, + "file %s is locally modified, but has been removed in revision %s as of %s", + finfo->fullname, jrev2, jdate2); + else + error (0, 0, + "file %s is locally modified, but has been removed in revision %s", + finfo->fullname, jrev2); + + /* FIXME: Should we arrange to return a non-zero exit + status? */ + + if (rev1 != NULL) + free (rev1); + + return; + } + + /* If only one join tag was specified, and the user file has + been changed since the greatest common ancestor (rev1), + then there is a conflict we can not resolve. See above for + the rationale. */ + if (join_rev2 == NULL + && strcmp (rev1, vers->vn_user) != 0) + { + if (jdate2 != NULL) + error (0, 0, + "file %s has been modified, but has been removed in revision %s as of %s", + finfo->fullname, jrev2, jdate2); + else + error (0, 0, + "file %s has been modified, but has been removed in revision %s", + finfo->fullname, jrev2); + + /* FIXME: Should we arrange to return a non-zero exit + status? */ + + if (rev1 != NULL) + free (rev1); + + return; + } + + if (rev1 != NULL) + free (rev1); + + /* The user file exists and has not been modified. Mark it + for removal. FIXME: If we are doing a checkout, this has + the effect of first checking out the file, and then + removing it. It would be better to just register the + removal. */ +#ifdef SERVER_SUPPORT + if (server_active) + { + server_scratch (finfo->file); + server_updated (finfo, vers, SERVER_UPDATED, (mode_t) -1, + (unsigned char *) NULL, (struct buffer *) NULL); + } +#endif + mrev = xmalloc (strlen (vers->vn_user) + 2); + sprintf (mrev, "-%s", vers->vn_user); + Register (finfo->entries, finfo->file, mrev, vers->ts_rcs, + vers->options, vers->tag, vers->date, vers->ts_conflict); + free (mrev); + /* We need to check existence_error here because if we are + running as the server, and the file is up to date in the + working directory, the client will not have sent us a copy. */ + if (unlink_file (finfo->file) < 0 && ! existence_error (errno)) + error (0, errno, "cannot remove file %s", finfo->fullname); +#ifdef SERVER_SUPPORT + if (server_active) + server_checked_in (finfo->file, finfo->update_dir, + finfo->repository); +#endif + if (! really_quiet) + error (0, 0, "scheduling %s for removal", finfo->fullname); + + return; + } + + /* If the target of the merge is the same as the working file + revision, then there is nothing to do. */ + if (vers->vn_user != NULL && strcmp (rev2, vers->vn_user) == 0) + { + if (rev1 != NULL) + free (rev1); + free (rev2); + return; + } + + /* If rev1 is dead or does not exist, then the file was added + between rev1 and rev2. */ + if (rev1 == NULL || RCS_isdead (vers->srcfile, rev1)) + { + if (rev1 != NULL) + free (rev1); + free (rev2); + + /* If the file does not exist in the working directory, then + we can just check out the new revision and mark it for + addition. */ + if (vers->vn_user == NULL) + { + Vers_TS *xvers; + + xvers = Version_TS (finfo, vers->options, jrev2, jdate2, 1, 0); + + /* FIXME: If checkout_file fails, we should arrange to + return a non-zero exit status. */ + status = checkout_file (finfo, xvers, 1, 0, 1); + + freevers_ts (&xvers); + + return; + } + + /* The file currently exists in the working directory, so we + have a conflict which we can not resolve. Note that this + is true even if the file is marked for addition or removal. */ + + if (jdate2 != NULL) + error (0, 0, + "file %s exists, but has been added in revision %s as of %s", + finfo->fullname, jrev2, jdate2); + else + error (0, 0, + "file %s exists, but has been added in revision %s", + finfo->fullname, jrev2); + + return; + } + + /* If the two merge revisions are the same, then there is nothing + to do. */ + if (strcmp (rev1, rev2) == 0) + { + free (rev1); + free (rev2); + return; + } + + /* If there is no working file, then we can't do the merge. */ + if (vers->vn_user == NULL) + { + free (rev1); + free (rev2); + + if (jdate2 != NULL) + error (0, 0, + "file %s is present in revision %s as of %s", + finfo->fullname, jrev2, jdate2); + else + error (0, 0, + "file %s is present in revision %s", + finfo->fullname, jrev2); + + /* FIXME: Should we arrange to return a non-zero exit status? */ + + return; + } + +#ifdef SERVER_SUPPORT + if (server_active && !isreadable (finfo->file)) + { + int retcode; + /* The file is up to date. Need to check out the current contents. */ + retcode = RCS_checkout (vers->srcfile, finfo->file, + vers->vn_user, (char *) NULL, + (char *) NULL, RUN_TTY, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + if (retcode != 0) + error (1, 0, + "failed to check out %s file", finfo->fullname); + } +#endif + + /* + * The users currently modified file is moved to a backup file name + * ".#filename.version", so that it will stay around for a few days + * before being automatically removed by some cron daemon. The "version" + * is the version of the file that the user was most up-to-date with + * before the merge. + */ + backup = xmalloc (strlen (finfo->file) + + strlen (vers->vn_user) + + sizeof (BAKPREFIX) + + 10); + (void) sprintf (backup, "%s%s.%s", BAKPREFIX, finfo->file, vers->vn_user); + + if (unlink_file (backup) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", backup); + copy_file (finfo->file, backup); + xchmod (finfo->file, 1); + + options = vers->options; +#if 0 + if (*options == '\0') + options = "-kk"; /* to ignore keyword expansions */ +#endif + + /* If the source of the merge is the same as the working file + revision, then we can just RCS_checkout the target (no merging + as such). In the text file case, this is probably quite + similar to the RCS_merge, but in the binary file case, + RCS_merge gives all kinds of trouble. */ + if (vers->vn_user != NULL + && strcmp (rev1, vers->vn_user) == 0 + /* See comments above about how No_Difference has already been + called. */ + && vers->ts_user != NULL + && strcmp (vers->ts_user, vers->ts_rcs) == 0 + + /* This is because of the worry below about $Name. If that + isn't a problem, I suspect this code probably works for + text files too. */ + && (strcmp (options, "-kb") == 0 + || wrap_merge_is_copy (finfo->file))) + { + /* FIXME: what about nametag? What does RCS_merge do with + $Name? */ + if (RCS_checkout (finfo->rcs, finfo->file, rev2, NULL, options, + RUN_TTY, (RCSCHECKOUTPROC)0, NULL) != 0) + status = 2; + else + status = 0; + + /* OK, this is really stupid. RCS_checkout carefully removes + write permissions, and we carefully put them back. But + until someone gets around to fixing it, that seems like the + easiest way to get what would seem to be the right mode. + I don't check CVSWRITE or _watched; I haven't thought about + that in great detail, but it seems like a watched file should + be checked out (writable) after a merge. */ + xchmod (finfo->file, 1); + + /* Traditionally, the text file case prints a whole bunch of + scary looking and verbose output which fails to tell the user + what is really going on (it gives them rev1 and rev2 but doesn't + indicate in any way that rev1 == vn_user). I think just a + simple "U foo" is good here; it seems analogous to the case in + which the file was added on the branch in terms of what to + print. */ + write_letter (finfo, 'U'); + } + else if (strcmp (options, "-kb") == 0 + || wrap_merge_is_copy (finfo->file) + || special_file_mismatch (finfo, rev1, rev2)) + { + /* We are dealing with binary files, or files with a + permission/linkage mismatch, and real merging would + need to take place. This is a conflict. We give the user + the two files, and let them resolve it. It is possible + that we should require a "touch foo" or similar step before + we allow a checkin. */ + if (RCS_checkout (finfo->rcs, finfo->file, rev2, NULL, options, + RUN_TTY, (RCSCHECKOUTPROC)0, NULL) != 0) + status = 2; + else + status = 0; + + /* OK, this is really stupid. RCS_checkout carefully removes + write permissions, and we carefully put them back. But + until someone gets around to fixing it, that seems like the + easiest way to get what would seem to be the right mode. + I don't check CVSWRITE or _watched; I haven't thought about + that in great detail, but it seems like a watched file should + be checked out (writable) after a merge. */ + xchmod (finfo->file, 1); + + /* Hmm. We don't give them REV1 anywhere. I guess most people + probably don't have a 3-way merge tool for the file type in + question, and might just get confused if we tried to either + provide them with a copy of the file from REV1, or even just + told them what REV1 is so they can get it themself, but it + might be worth thinking about. */ + /* See comment in merge_file about the "nonmergeable file" + terminology. */ + error (0, 0, "nonmergeable file needs merge"); + error (0, 0, "revision %s from repository is now in %s", + rev2, finfo->fullname); + error (0, 0, "file from working directory is now in %s", backup); + write_letter (finfo, 'C'); + } + else + status = RCS_merge (finfo->rcs, vers->srcfile->path, finfo->file, + options, rev1, rev2); + + if (status != 0 && status != 1) + { + error (0, status == -1 ? errno : 0, + "could not merge revision %s of %s", rev2, finfo->fullname); + error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s", + finfo->fullname, backup); + rename_file (backup, finfo->file); + } + free (rev1); + free (rev2); + + /* The file has changed, but if we just checked it out it may + still have the same timestamp it did when it was first + registered above in checkout_file. We register it again with a + dummy timestamp to make sure that later runs of CVS will + recognize that it has changed. + + We don't actually need to register again if we called + RCS_checkout above, and we aren't running as the server. + However, that is not the normal case, and calling Register + again won't cost much in that case. */ + { + char *cp = 0; + + if (status) + { + (void) time (&last_register_time); + cp = time_stamp (finfo->file); + } + Register (finfo->entries, finfo->file, vers->vn_rcs, + "Result of merge", vers->options, vers->tag, + vers->date, cp); + if (cp) + free(cp); + } + +#ifdef SERVER_SUPPORT + if (server_active) + { + server_copy_file (finfo->file, finfo->update_dir, finfo->repository, + backup); + server_updated (finfo, vers, SERVER_MERGED, + (mode_t) -1, (unsigned char *) NULL, + (struct buffer *) NULL); + } +#endif + free (backup); +} + +/* + * Report whether revisions REV1 and REV2 of FINFO agree on: + * . file ownership + * . permissions + * . major and minor device numbers + * . symbolic links + * . hard links + * + * If either REV1 or REV2 is NULL, the working copy is used instead. + * + * Return 1 if the files differ on these data. + */ + +int +special_file_mismatch (finfo, rev1, rev2) + struct file_info *finfo; + char *rev1; + char *rev2; +{ +#ifdef PRESERVE_PERMISSIONS_SUPPORT + struct stat sb; + RCSVers *vp; + Node *n; + uid_t rev1_uid, rev2_uid; + gid_t rev1_gid, rev2_gid; + mode_t rev1_mode, rev2_mode; + unsigned long dev_long; + dev_t rev1_dev, rev2_dev; + char *rev1_symlink = NULL; + char *rev2_symlink = NULL; + List *rev1_hardlinks = NULL; + List *rev2_hardlinks = NULL; + int check_uids, check_gids, check_modes; + int result; + + /* If we don't care about special file info, then + don't report a mismatch in any case. */ + if (!preserve_perms) + return 0; + + /* When special_file_mismatch is called from No_Difference, the + RCS file has been only partially parsed. We must read the + delta tree in order to compare special file info recorded in + the delta nodes. (I think this is safe. -twp) */ + if (finfo->rcs->flags & PARTIAL) + RCS_reparsercsfile (finfo->rcs, NULL, NULL); + + check_uids = check_gids = check_modes = 1; + + /* Obtain file information for REV1. If this is null, then stat + finfo->file and use that info. */ + /* If a revision does not know anything about its status, + then presumably it doesn't matter, and indicates no conflict. */ + + if (rev1 == NULL) + { + if (islink (finfo->file)) + rev1_symlink = xreadlink (finfo->file); + else + { + if (CVS_LSTAT (finfo->file, &sb) < 0) + error (1, errno, "could not get file information for %s", + finfo->file); + rev1_uid = sb.st_uid; + rev1_gid = sb.st_gid; + rev1_mode = sb.st_mode; + if (S_ISBLK (rev1_mode) || S_ISCHR (rev1_mode)) + rev1_dev = sb.st_rdev; + } + rev1_hardlinks = list_linked_files_on_disk (finfo->file); + } + else + { + n = findnode (finfo->rcs->versions, rev1); + vp = (RCSVers *) n->data; + + n = findnode (vp->other_delta, "symlink"); + if (n != NULL) + rev1_symlink = xstrdup (n->data); + else + { + n = findnode (vp->other_delta, "owner"); + if (n == NULL) + check_uids = 0; /* don't care */ + else + rev1_uid = strtoul (n->data, NULL, 10); + + n = findnode (vp->other_delta, "group"); + if (n == NULL) + check_gids = 0; /* don't care */ + else + rev1_gid = strtoul (n->data, NULL, 10); + + n = findnode (vp->other_delta, "permissions"); + if (n == NULL) + check_modes = 0; /* don't care */ + else + rev1_mode = strtoul (n->data, NULL, 8); + + n = findnode (vp->other_delta, "special"); + if (n == NULL) + rev1_mode |= S_IFREG; + else + { + /* If the size of `ftype' changes, fix the sscanf call also */ + char ftype[16]; + if (sscanf (n->data, "%16s %lu", ftype, + &dev_long) < 2) + error (1, 0, "%s:%s has bad `special' newphrase %s", + finfo->file, rev1, n->data); + rev1_dev = dev_long; + if (strcmp (ftype, "character") == 0) + rev1_mode |= S_IFCHR; + else if (strcmp (ftype, "block") == 0) + rev1_mode |= S_IFBLK; + else + error (0, 0, "%s:%s unknown file type `%s'", + finfo->file, rev1, ftype); + } + + rev1_hardlinks = vp->hardlinks; + if (rev1_hardlinks == NULL) + rev1_hardlinks = getlist(); + } + } + + /* Obtain file information for REV2. */ + if (rev2 == NULL) + { + if (islink (finfo->file)) + rev2_symlink = xreadlink (finfo->file); + else + { + if (CVS_LSTAT (finfo->file, &sb) < 0) + error (1, errno, "could not get file information for %s", + finfo->file); + rev2_uid = sb.st_uid; + rev2_gid = sb.st_gid; + rev2_mode = sb.st_mode; + if (S_ISBLK (rev2_mode) || S_ISCHR (rev2_mode)) + rev2_dev = sb.st_rdev; + } + rev2_hardlinks = list_linked_files_on_disk (finfo->file); + } + else + { + n = findnode (finfo->rcs->versions, rev2); + vp = (RCSVers *) n->data; + + n = findnode (vp->other_delta, "symlink"); + if (n != NULL) + rev2_symlink = xstrdup (n->data); + else + { + n = findnode (vp->other_delta, "owner"); + if (n == NULL) + check_uids = 0; /* don't care */ + else + rev2_uid = strtoul (n->data, NULL, 10); + + n = findnode (vp->other_delta, "group"); + if (n == NULL) + check_gids = 0; /* don't care */ + else + rev2_gid = strtoul (n->data, NULL, 10); + + n = findnode (vp->other_delta, "permissions"); + if (n == NULL) + check_modes = 0; /* don't care */ + else + rev2_mode = strtoul (n->data, NULL, 8); + + n = findnode (vp->other_delta, "special"); + if (n == NULL) + rev2_mode |= S_IFREG; + else + { + /* If the size of `ftype' changes, fix the sscanf call also */ + char ftype[16]; + if (sscanf (n->data, "%16s %lu", ftype, + &dev_long) < 2) + error (1, 0, "%s:%s has bad `special' newphrase %s", + finfo->file, rev2, n->data); + rev2_dev = dev_long; + if (strcmp (ftype, "character") == 0) + rev2_mode |= S_IFCHR; + else if (strcmp (ftype, "block") == 0) + rev2_mode |= S_IFBLK; + else + error (0, 0, "%s:%s unknown file type `%s'", + finfo->file, rev2, ftype); + } + + rev2_hardlinks = vp->hardlinks; + if (rev2_hardlinks == NULL) + rev2_hardlinks = getlist(); + } + } + + /* Check the user/group ownerships and file permissions, printing + an error for each mismatch found. Return 0 if all characteristics + matched, and 1 otherwise. */ + + result = 0; + + /* Compare symlinks first, since symlinks are simpler (don't have + any other characteristics). */ + if (rev1_symlink != NULL && rev2_symlink == NULL) + { + error (0, 0, "%s is a symbolic link", + (rev1 == NULL ? "working file" : rev1)); + result = 1; + } + else if (rev1_symlink == NULL && rev2_symlink != NULL) + { + error (0, 0, "%s is a symbolic link", + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + else if (rev1_symlink != NULL) + result = (strcmp (rev1_symlink, rev2_symlink) == 0); + else + { + /* Compare user ownership. */ + if (check_uids && rev1_uid != rev2_uid) + { + error (0, 0, "%s: owner mismatch between %s and %s", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + + /* Compare group ownership. */ + if (check_gids && rev1_gid != rev2_gid) + { + error (0, 0, "%s: group mismatch between %s and %s", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + + /* Compare permissions. */ + if (check_modes && + (rev1_mode & 07777) != (rev2_mode & 07777)) + { + error (0, 0, "%s: permission mismatch between %s and %s", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + + /* Compare device file characteristics. */ + if ((rev1_mode & S_IFMT) != (rev2_mode & S_IFMT)) + { + error (0, 0, "%s: %s and %s are different file types", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + else if (S_ISBLK (rev1_mode)) + { + if (rev1_dev != rev2_dev) + { + error (0, 0, "%s: device numbers of %s and %s do not match", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + } + + /* Compare hard links. */ + if (compare_linkage_lists (rev1_hardlinks, rev2_hardlinks) == 0) + { + error (0, 0, "%s: hard linkage of %s and %s do not match", + finfo->file, + (rev1 == NULL ? "working file" : rev1), + (rev2 == NULL ? "working file" : rev2)); + result = 1; + } + } + + if (rev1_symlink != NULL) + free (rev1_symlink); + if (rev2_symlink != NULL) + free (rev2_symlink); + if (rev1_hardlinks != NULL) + dellist (&rev1_hardlinks); + if (rev2_hardlinks != NULL) + dellist (&rev2_hardlinks); + + return result; +#else + return 0; +#endif +} + +int +joining () +{ + return (join_rev1 != NULL); +} diff --git a/gnu/dist/cvs/src/vers_ts.c b/gnu/dist/cvs/src/vers_ts.c new file mode 100644 index 00000000000..e1ba32d2c4c --- /dev/null +++ b/gnu/dist/cvs/src/vers_ts.c @@ -0,0 +1,406 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS source distribution. + */ + +#include "cvs.h" + +#ifdef SERVER_SUPPORT +static void time_stamp_server PROTO((char *, Vers_TS *, Entnode *)); +#endif + +/* Fill in and return a Vers_TS structure for the file FINFO. TAG and + DATE are from the command line. */ + +Vers_TS * +Version_TS (finfo, options, tag, date, force_tag_match, set_time) + struct file_info *finfo; + + /* Keyword expansion options, I think generally from the command + line. Can be either NULL or "" to indicate none are specified + here. */ + char *options; + char *tag; + char *date; + int force_tag_match; + int set_time; +{ + Node *p; + RCSNode *rcsdata; + Vers_TS *vers_ts; + struct stickydirtag *sdtp; + Entnode *entdata; + +#ifdef UTIME_EXPECTS_WRITABLE + int change_it_back = 0; +#endif + + /* get a new Vers_TS struct */ + vers_ts = (Vers_TS *) xmalloc (sizeof (Vers_TS)); + memset ((char *) vers_ts, 0, sizeof (*vers_ts)); + + /* + * look up the entries file entry and fill in the version and timestamp + * if entries is NULL, there is no entries file so don't bother trying to + * look it up (used by checkout -P) + */ + if (finfo->entries == NULL) + { + sdtp = NULL; + p = NULL; + } + else + { + p = findnode_fn (finfo->entries, finfo->file); + sdtp = (struct stickydirtag *) finfo->entries->list->data; /* list-private */ + } + + entdata = NULL; + if (p != NULL) + { + entdata = (Entnode *) p->data; + + if (entdata->type == ENT_SUBDIR) + { + /* According to cvs.texinfo, the various fields in the Entries + file for a directory (other than the name) do not have a + defined meaning. We need to pass them along without getting + confused based on what is in them. Therefore we make sure + not to set vn_user and the like from Entries, add.c and + perhaps other code will expect these fields to be NULL for + a directory. */ + vers_ts->entdata = entdata; + } + else +#ifdef SERVER_SUPPORT + /* An entries line with "D" in the timestamp indicates that the + client sent Is-modified without sending Entry. So we want to + use the entries line for the sole purpose of telling + time_stamp_server what is up; we don't want the rest of CVS + to think there is an entries line. */ + if (strcmp (entdata->timestamp, "D") != 0) +#endif + { + vers_ts->vn_user = xstrdup (entdata->version); + vers_ts->ts_rcs = xstrdup (entdata->timestamp); + vers_ts->ts_conflict = xstrdup (entdata->conflict); + if (!tag) + { + if (!(sdtp && sdtp->aflag)) + vers_ts->tag = xstrdup (entdata->tag); + } + if (!date) + { + if (!(sdtp && sdtp->aflag)) + vers_ts->date = xstrdup (entdata->date); + } + vers_ts->entdata = entdata; + } + /* Even if we don't have an "entries line" as such + (vers_ts->entdata), we want to pick up options which could + have been from a Kopt protocol request. */ + if (!options || (options && *options == '\0')) + { + if (!(sdtp && sdtp->aflag)) + vers_ts->options = xstrdup (entdata->options); + } + } + + /* + * -k options specified on the command line override (and overwrite) + * options stored in the entries file + */ + if (options && *options != '\0') + vers_ts->options = xstrdup (options); + else if (!vers_ts->options || *vers_ts->options == '\0') + { + if (finfo->rcs != NULL) + { + /* If no keyword expansion was specified on command line, + use whatever was in the rcs file (if there is one). This + is how we, if we are the server, tell the client whether + a file is binary. */ + char *rcsexpand = RCS_getexpand (finfo->rcs); + if (rcsexpand != NULL) + { + vers_ts->options = xmalloc (strlen (rcsexpand) + 3); + strcpy (vers_ts->options, "-k"); + strcat (vers_ts->options, rcsexpand); + } + } + } + if (!vers_ts->options) + vers_ts->options = xstrdup (""); + + /* + * if tags were specified on the command line, they override what is in + * the Entries file + */ + if (tag || date) + { + vers_ts->tag = xstrdup (tag); + vers_ts->date = xstrdup (date); + } + else if (!vers_ts->entdata && (sdtp && sdtp->aflag == 0)) + { + if (!vers_ts->tag) + { + vers_ts->tag = xstrdup (sdtp->tag); + vers_ts->nonbranch = sdtp->nonbranch; + } + if (!vers_ts->date) + vers_ts->date = xstrdup (sdtp->date); + } + + /* Now look up the info on the source controlled file */ + if (finfo->rcs != NULL) + { + rcsdata = finfo->rcs; + rcsdata->refcount++; + } + else if (finfo->repository != NULL) + rcsdata = RCS_parse (finfo->file, finfo->repository); + else + rcsdata = NULL; + + if (rcsdata != NULL) + { + /* squirrel away the rcsdata pointer for others */ + vers_ts->srcfile = rcsdata; + + if (vers_ts->tag && strcmp (vers_ts->tag, TAG_BASE) == 0) + { + vers_ts->vn_rcs = xstrdup (vers_ts->vn_user); + vers_ts->vn_tag = xstrdup (vers_ts->vn_user); + } + else + { + int simple; + + vers_ts->vn_rcs = RCS_getversion (rcsdata, vers_ts->tag, + vers_ts->date, force_tag_match, + &simple); + if (vers_ts->vn_rcs == NULL) + vers_ts->vn_tag = NULL; + else if (simple) + vers_ts->vn_tag = xstrdup (vers_ts->tag); + else + vers_ts->vn_tag = xstrdup (vers_ts->vn_rcs); + } + + /* + * If the source control file exists and has the requested revision, + * get the Date the revision was checked in. If "user" exists, set + * its mtime. + */ + if (set_time && vers_ts->vn_rcs != NULL) + { +#ifdef SERVER_SUPPORT + if (server_active) + server_modtime (finfo, vers_ts); + else +#endif + { + struct utimbuf t; + + memset (&t, 0, sizeof (t)); + t.modtime = + RCS_getrevtime (rcsdata, vers_ts->vn_rcs, 0, 0); + if (t.modtime != (time_t) -1) + { + t.actime = t.modtime; + +#ifdef UTIME_EXPECTS_WRITABLE + if (!iswritable (finfo->file)) + { + xchmod (finfo->file, 1); + change_it_back = 1; + } +#endif /* UTIME_EXPECTS_WRITABLE */ + + /* This used to need to ignore existence_errors + (for cases like where update.c now clears + set_time if noexec, but didn't used to). I + think maybe now it doesn't (server_modtime does + not like those kinds of cases). */ + (void) utime (finfo->file, &t); + +#ifdef UTIME_EXPECTS_WRITABLE + if (change_it_back == 1) + { + xchmod (finfo->file, 0); + change_it_back = 0; + } +#endif /* UTIME_EXPECTS_WRITABLE */ + } + } + } + } + + /* get user file time-stamp in ts_user */ + if (finfo->entries != (List *) NULL) + { +#ifdef SERVER_SUPPORT + if (server_active) + time_stamp_server (finfo->file, vers_ts, entdata); + else +#endif + vers_ts->ts_user = time_stamp (finfo->file); + } + + return (vers_ts); +} + +#ifdef SERVER_SUPPORT + +/* Set VERS_TS->TS_USER to time stamp for FILE. */ + +/* Separate these out to keep the logic below clearer. */ +#define mark_lost(V) ((V)->ts_user = 0) +#define mark_unchanged(V) ((V)->ts_user = xstrdup ((V)->ts_rcs)) + +static void +time_stamp_server (file, vers_ts, entdata) + char *file; + Vers_TS *vers_ts; + Entnode *entdata; +{ + struct stat sb; + char *cp; + + if (CVS_LSTAT (file, &sb) < 0) + { + if (! existence_error (errno)) + error (1, errno, "cannot stat temp file"); + + /* Missing file means lost or unmodified; check entries + file to see which. + + XXX FIXME - If there's no entries file line, we + wouldn't be getting the file at all, so consider it + lost. I don't know that that's right, but it's not + clear to me that either choice is. Besides, would we + have an RCS string in that case anyways? */ + if (entdata == NULL) + mark_lost (vers_ts); + else if (entdata->timestamp + && entdata->timestamp[0] == '=') + mark_unchanged (vers_ts); + else if (entdata->timestamp != NULL + && (entdata->timestamp[0] == 'M' + || entdata->timestamp[0] == 'D') + && entdata->timestamp[1] == '\0') + vers_ts->ts_user = xstrdup ("Is-modified"); + else + mark_lost (vers_ts); + } + else if (sb.st_mtime == 0) + { + /* We shouldn't reach this case any more! */ + abort (); + } + else + { + struct tm *tm_p; + struct tm local_tm; + + vers_ts->ts_user = xmalloc (25); + /* We want to use the same timestamp format as is stored in the + st_mtime. For unix (and NT I think) this *must* be universal + time (UT), so that files don't appear to be modified merely + because the timezone has changed. For VMS, or hopefully other + systems where gmtime returns NULL, the modification time is + stored in local time, and therefore it is not possible to cause + st_mtime to be out of sync by changing the timezone. */ + tm_p = gmtime (&sb.st_mtime); + if (tm_p) + { + memcpy (&local_tm, tm_p, sizeof (local_tm)); + cp = asctime (&local_tm); /* copy in the modify time */ + } + else + cp = ctime (&sb.st_mtime); + + cp[24] = 0; + (void) strcpy (vers_ts->ts_user, cp); + } +} + +#endif /* SERVER_SUPPORT */ +/* + * Gets the time-stamp for the file "file" and returns it in space it + * allocates + */ +char * +time_stamp (file) + char *file; +{ + struct stat sb; + char *cp; + char *ts; + + if (CVS_LSTAT (file, &sb) < 0) + { + ts = NULL; + } + else + { + struct tm *tm_p; + struct tm local_tm; + ts = xmalloc (25); + /* We want to use the same timestamp format as is stored in the + st_mtime. For unix (and NT I think) this *must* be universal + time (UT), so that files don't appear to be modified merely + because the timezone has changed. For VMS, or hopefully other + systems where gmtime returns NULL, the modification time is + stored in local time, and therefore it is not possible to cause + st_mtime to be out of sync by changing the timezone. */ + tm_p = gmtime (&sb.st_mtime); + if (tm_p) + { + memcpy (&local_tm, tm_p, sizeof (local_tm)); + cp = asctime (&local_tm); /* copy in the modify time */ + } + else + cp = ctime(&sb.st_mtime); + + cp[24] = 0; + (void) strcpy (ts, cp); + } + + return (ts); +} + +/* + * free up a Vers_TS struct + */ +void +freevers_ts (versp) + Vers_TS **versp; +{ + if ((*versp)->srcfile) + freercsnode (&((*versp)->srcfile)); + if ((*versp)->vn_user) + free ((*versp)->vn_user); + if ((*versp)->vn_rcs) + free ((*versp)->vn_rcs); + if ((*versp)->vn_tag) + free ((*versp)->vn_tag); + if ((*versp)->ts_user) + free ((*versp)->ts_user); + if ((*versp)->ts_rcs) + free ((*versp)->ts_rcs); + if ((*versp)->options) + free ((*versp)->options); + if ((*versp)->tag) + free ((*versp)->tag); + if ((*versp)->date) + free ((*versp)->date); + if ((*versp)->ts_conflict) + free ((*versp)->ts_conflict); + free ((char *) *versp); + *versp = (Vers_TS *) NULL; +} diff --git a/gnu/dist/cvs/src/version.c b/gnu/dist/cvs/src/version.c new file mode 100644 index 00000000000..a4401dad4a0 --- /dev/null +++ b/gnu/dist/cvs/src/version.c @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1994 david d `zoo' zuhn + * Copyright (c) 1994 Free Software Foundation, Inc. + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with this CVS source distribution. + * + * version.c - the CVS version number + */ + +#include "cvs.h" + +char *version_string = "\nConcurrent Versions System (CVS) 1.10.8"; + +#ifdef CLIENT_SUPPORT +#ifdef SERVER_SUPPORT +char *config_string = " (client/server)\n"; +#else +char *config_string = " (client)\n"; +#endif +#else +#ifdef SERVER_SUPPORT +char *config_string = " (server)\n"; +#else +char *config_string = "\n"; +#endif +#endif diff --git a/gnu/dist/cvs/src/watch.c b/gnu/dist/cvs/src/watch.c new file mode 100644 index 00000000000..b2935ac3dd0 --- /dev/null +++ b/gnu/dist/cvs/src/watch.c @@ -0,0 +1,529 @@ +/* Implementation for "cvs watch add", "cvs watchers", and related commands + + 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, 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. */ + +#include "cvs.h" +#include "edit.h" +#include "fileattr.h" +#include "watch.h" + +const char *const watch_usage[] = +{ + "Usage: %s %s [on|off|add|remove] [-lR] [-a action] [files...]\n", + "on/off: turn on/off read-only checkouts of files\n", + "add/remove: add or remove notification on actions\n", + "-l (on/off/add/remove): Local directory only, not recursive\n", + "-R (on/off/add/remove): Process directories recursively\n", + "-a (add/remove): Specify what actions, one of\n", + " edit,unedit,commit,all,none\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +static struct addremove_args the_args; + +void +watch_modify_watchers (file, what) + char *file; + struct addremove_args *what; +{ + char *curattr = fileattr_get0 (file, "_watchers"); + char *p; + char *pend; + char *nextp; + char *who; + int who_len; + char *mycurattr; + char *mynewattr; + size_t mynewattr_size; + + int add_edit_pending; + int add_unedit_pending; + int add_commit_pending; + int remove_edit_pending; + int remove_unedit_pending; + int remove_commit_pending; + int add_tedit_pending; + int add_tunedit_pending; + int add_tcommit_pending; + + who = getcaller (); + who_len = strlen (who); + + /* Look for current watcher types for this user. */ + mycurattr = NULL; + if (curattr != NULL) + { + p = curattr; + while (1) { + if (strncmp (who, p, who_len) == 0 + && p[who_len] == '>') + { + /* Found this user. */ + mycurattr = p + who_len + 1; + } + p = strchr (p, ','); + if (p == NULL) + break; + ++p; + } + } + if (mycurattr != NULL) + { + mycurattr = xstrdup (mycurattr); + p = strchr (mycurattr, ','); + if (p != NULL) + *p = '\0'; + } + + /* Now copy mycurattr to mynewattr, making the requisite modifications. + Note that we add a dummy '+' to the start of mynewattr, to reduce + special cases (but then we strip it off when we are done). */ + + mynewattr_size = sizeof "+edit+unedit+commit+tedit+tunedit+tcommit"; + if (mycurattr != NULL) + mynewattr_size += strlen (mycurattr); + mynewattr = xmalloc (mynewattr_size); + mynewattr[0] = '\0'; + + add_edit_pending = what->adding && what->edit; + add_unedit_pending = what->adding && what->unedit; + add_commit_pending = what->adding && what->commit; + remove_edit_pending = !what->adding && what->edit; + remove_unedit_pending = !what->adding && what->unedit; + remove_commit_pending = !what->adding && what->commit; + add_tedit_pending = what->add_tedit; + add_tunedit_pending = what->add_tunedit; + add_tcommit_pending = what->add_tcommit; + + /* Copy over existing watch types, except those to be removed. */ + p = mycurattr; + while (p != NULL) + { + pend = strchr (p, '+'); + if (pend == NULL) + { + pend = p + strlen (p); + nextp = NULL; + } + else + nextp = pend + 1; + + /* Process this item. */ + if (pend - p == 4 && strncmp ("edit", p, 4) == 0) + { + if (!remove_edit_pending) + strcat (mynewattr, "+edit"); + add_edit_pending = 0; + } + else if (pend - p == 6 && strncmp ("unedit", p, 6) == 0) + { + if (!remove_unedit_pending) + strcat (mynewattr, "+unedit"); + add_unedit_pending = 0; + } + else if (pend - p == 6 && strncmp ("commit", p, 6) == 0) + { + if (!remove_commit_pending) + strcat (mynewattr, "+commit"); + add_commit_pending = 0; + } + else if (pend - p == 5 && strncmp ("tedit", p, 5) == 0) + { + if (!what->remove_temp) + strcat (mynewattr, "+tedit"); + add_tedit_pending = 0; + } + else if (pend - p == 7 && strncmp ("tunedit", p, 7) == 0) + { + if (!what->remove_temp) + strcat (mynewattr, "+tunedit"); + add_tunedit_pending = 0; + } + else if (pend - p == 7 && strncmp ("tcommit", p, 7) == 0) + { + if (!what->remove_temp) + strcat (mynewattr, "+tcommit"); + add_tcommit_pending = 0; + } + else + { + char *mp; + + /* Copy over any unrecognized watch types, for future + expansion. */ + mp = mynewattr + strlen (mynewattr); + *mp++ = '+'; + strncpy (mp, p, pend - p); + *(mp + (pend - p)) = '\0'; + } + + /* Set up for next item. */ + p = nextp; + } + + /* Add in new watch types. */ + if (add_edit_pending) + strcat (mynewattr, "+edit"); + if (add_unedit_pending) + strcat (mynewattr, "+unedit"); + if (add_commit_pending) + strcat (mynewattr, "+commit"); + if (add_tedit_pending) + strcat (mynewattr, "+tedit"); + if (add_tunedit_pending) + strcat (mynewattr, "+tunedit"); + if (add_tcommit_pending) + strcat (mynewattr, "+tcommit"); + + { + char *curattr_new; + + curattr_new = + fileattr_modify (curattr, + who, + mynewattr[0] == '\0' ? NULL : mynewattr + 1, + '>', + ','); + /* If the attribute is unchanged, don't rewrite the attribute file. */ + if (!((curattr_new == NULL && curattr == NULL) + || (curattr_new != NULL + && curattr != NULL + && strcmp (curattr_new, curattr) == 0))) + fileattr_set (file, + "_watchers", + curattr_new); + if (curattr_new != NULL) + free (curattr_new); + } + + if (curattr != NULL) + free (curattr); + if (mycurattr != NULL) + free (mycurattr); + if (mynewattr != NULL) + free (mynewattr); +} + +static int addremove_fileproc PROTO ((void *callerdat, + struct file_info *finfo)); + +static int +addremove_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + watch_modify_watchers (finfo->file, &the_args); + return 0; +} + +static int addremove_filesdoneproc PROTO ((void *, int, char *, char *, + List *)); + +static int +addremove_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + char *repository; + char *update_dir; + List *entries; +{ + if (the_args.setting_default) + watch_modify_watchers (NULL, &the_args); + return err; +} + +static int watch_addremove PROTO ((int argc, char **argv)); + +static int +watch_addremove (argc, argv) + int argc; + char **argv; +{ + int c; + int local = 0; + int err; + int a_omitted; + + a_omitted = 1; + the_args.commit = 0; + the_args.edit = 0; + the_args.unedit = 0; + optind = 0; + while ((c = getopt (argc, argv, "+lRa:")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'a': + a_omitted = 0; + if (strcmp (optarg, "edit") == 0) + the_args.edit = 1; + else if (strcmp (optarg, "unedit") == 0) + the_args.unedit = 1; + else if (strcmp (optarg, "commit") == 0) + the_args.commit = 1; + else if (strcmp (optarg, "all") == 0) + { + the_args.edit = 1; + the_args.unedit = 1; + the_args.commit = 1; + } + else if (strcmp (optarg, "none") == 0) + { + the_args.edit = 0; + the_args.unedit = 0; + the_args.commit = 0; + } + else + usage (watch_usage); + break; + case '?': + default: + usage (watch_usage); + break; + } + } + argc -= optind; + argv += optind; + + if (a_omitted) + { + the_args.edit = 1; + the_args.unedit = 1; + the_args.commit = 1; + } + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + ign_setup (); + + if (local) + send_arg ("-l"); + /* FIXME: copes poorly with "all" if server is extended to have + new watch types and client is still running an old version. */ + if (the_args.edit) + { + send_arg ("-a"); + send_arg ("edit"); + } + if (the_args.unedit) + { + send_arg ("-a"); + send_arg ("unedit"); + } + if (the_args.commit) + { + send_arg ("-a"); + send_arg ("commit"); + } + if (!the_args.edit && !the_args.unedit && !the_args.commit) + { + send_arg ("-a"); + send_arg ("none"); + } + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server (the_args.adding ? + "watch-add\012" : "watch-remove\012", + 0); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + the_args.setting_default = (argc <= 0); + + lock_tree_for_write (argc, argv, local, 0); + + err = start_recursion (addremove_fileproc, addremove_filesdoneproc, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, + 1); + + Lock_Cleanup (); + return err; +} + +int +watch_add (argc, argv) + int argc; + char **argv; +{ + the_args.adding = 1; + return watch_addremove (argc, argv); +} + +int +watch_remove (argc, argv) + int argc; + char **argv; +{ + the_args.adding = 0; + return watch_addremove (argc, argv); +} + +int +watch (argc, argv) + int argc; + char **argv; +{ + if (argc <= 1) + usage (watch_usage); + if (strcmp (argv[1], "on") == 0) + { + --argc; + ++argv; + return watch_on (argc, argv); + } + else if (strcmp (argv[1], "off") == 0) + { + --argc; + ++argv; + return watch_off (argc, argv); + } + else if (strcmp (argv[1], "add") == 0) + { + --argc; + ++argv; + return watch_add (argc, argv); + } + else if (strcmp (argv[1], "remove") == 0) + { + --argc; + ++argv; + return watch_remove (argc, argv); + } + else + usage (watch_usage); + return 0; +} + +static const char *const watchers_usage[] = +{ + "Usage: %s %s [-lR] [files...]\n", + "\t-l\tProcess this directory only (not recursive).\n", + "\t-R\tProcess directories recursively.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +static int watchers_fileproc PROTO ((void *callerdat, + struct file_info *finfo)); + +static int +watchers_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + char *them; + char *p; + + them = fileattr_get0 (finfo->file, "_watchers"); + if (them == NULL) + return 0; + + cvs_output (finfo->fullname, 0); + + p = them; + while (1) + { + cvs_output ("\t", 1); + while (*p != '>' && *p != '\0') + cvs_output (p++, 1); + if (*p == '\0') + { + /* Only happens if attribute is misformed. */ + cvs_output ("\n", 1); + break; + } + ++p; + cvs_output ("\t", 1); + while (1) + { + while (*p != '+' && *p != ',' && *p != '\0') + cvs_output (p++, 1); + if (*p == '\0') + { + cvs_output ("\n", 1); + goto out; + } + if (*p == ',') + { + ++p; + break; + } + ++p; + cvs_output ("\t", 1); + } + cvs_output ("\n", 1); + } + out:; + return 0; +} + +int +watchers (argc, argv) + int argc; + char **argv; +{ + int local = 0; + int c; + + if (argc == -1) + usage (watchers_usage); + + optind = 0; + while ((c = getopt (argc, argv, "+lR")) != -1) + { + switch (c) + { + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case '?': + default: + usage (watchers_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + start_server (); + ign_setup (); + + if (local) + send_arg ("-l"); + send_files (argc, argv, local, 0, SEND_NO_CONTENTS); + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_to_server ("watchers\012", 0); + return get_responses_and_close (); + } +#endif /* CLIENT_SUPPORT */ + + return start_recursion (watchers_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, + argc, argv, local, W_LOCAL, 0, 1, (char *)NULL, + 1); +} diff --git a/gnu/dist/cvs/src/wrapper.c b/gnu/dist/cvs/src/wrapper.c new file mode 100644 index 00000000000..2d53c7a981d --- /dev/null +++ b/gnu/dist/cvs/src/wrapper.c @@ -0,0 +1,614 @@ +/* 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, 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. */ + +#include "cvs.h" +#include "getline.h" + +/* + Original Author: athan@morgan.com <Andrew C. Athan> 2/1/94 + Modified By: vdemarco@bou.shl.com + + This package was written to support the NEXTSTEP concept of + "wrappers." These are essentially directories that are to be + treated as "files." This package allows such wrappers to be + "processed" on the way in and out of CVS. The intended use is to + wrap up a wrapper into a single tar, such that that tar can be + treated as a single binary file in CVS. To solve the problem + effectively, it was also necessary to be able to prevent rcsmerge + application at appropriate times. + + ------------------ + Format of wrapper file ($CVSROOT/CVSROOT/cvswrappers or .cvswrappers) + + wildcard [option value][option value]... + + where option is one of + -f from cvs filter value: path to filter + -t to cvs filter value: path to filter + -m update methodology value: MERGE or COPY + -k default -k rcs option to use on import or add + + and value is a single-quote delimited value. + + E.g: + *.nib -f 'gunzipuntar' -t 'targzip' -m 'COPY' +*/ + + +typedef struct { + char *wildCard; + char *tocvsFilter; + char *fromcvsFilter; + char *rcsOption; + WrapMergeMethod mergeMethod; +} WrapperEntry; + +static WrapperEntry **wrap_list=NULL; +static WrapperEntry **wrap_saved_list=NULL; + +static int wrap_size=0; +static int wrap_count=0; +static int wrap_tempcount=0; + +/* FIXME: the relationship between wrap_count, wrap_tempcount, + * wrap_saved_count, and wrap_saved_tempcount is not entirely clear; + * it is certainly suspicious that wrap_saved_count is never set to a + * value other than zero! If the variable isn't being used, it should + * be removed. And in general, we should describe how temporary + * vs. permanent wrappers are implemented, and then make sure the + * implementation is actually doing that. + * + * Right now things seem to be working, but that's no guarantee there + * isn't a bug lurking somewhere in the murk. + */ + +static int wrap_saved_count=0; + +static int wrap_saved_tempcount=0; + +#define WRAPPER_GROW 8 + +void wrap_add_entry PROTO((WrapperEntry *e,int temp)); +void wrap_kill PROTO((void)); +void wrap_kill_temp PROTO((void)); +void wrap_free_entry PROTO((WrapperEntry *e)); +void wrap_free_entry_internal PROTO((WrapperEntry *e)); +void wrap_restore_saved PROTO((void)); + +void wrap_setup() +{ + /* FIXME-reentrancy: if we do a multithreaded server, will need to + move this to a per-connection data structure, or better yet + think about a cleaner solution. */ + static int wrap_setup_already_done = 0; + char *homedir; + + if (wrap_setup_already_done != 0) + return; + else + wrap_setup_already_done = 1; + +#ifdef CLIENT_SUPPORT + if (!client_active) +#endif + { + char *file; + + file = xmalloc (strlen (CVSroot_directory) + + sizeof (CVSROOTADM) + + sizeof (CVSROOTADM_WRAPPER) + + 10); + /* Then add entries found in repository, if it exists. */ + (void) sprintf (file, "%s/%s/%s", CVSroot_directory, CVSROOTADM, + CVSROOTADM_WRAPPER); + if (isfile (file)) + { + wrap_add_file(file,0); + } + free (file); + } + + /* Then add entries found in home dir, (if user has one) and file + exists. */ + homedir = get_homedir (); + /* If we can't find a home directory, ignore ~/.cvswrappers. This may + make tracking down problems a bit of a pain, but on the other + hand it might be obnoxious to complain when CVS will function + just fine without .cvswrappers (and many users won't even know what + .cvswrappers is). */ + if (homedir != NULL) + { + char *file; + + file = xmalloc (strlen (homedir) + sizeof (CVSDOTWRAPPER) + 10); + (void) sprintf (file, "%s/%s", homedir, CVSDOTWRAPPER); + if (isfile (file)) + { + wrap_add_file (file, 0); + } + free (file); + } + + /* FIXME: calling wrap_add() below implies that the CVSWRAPPERS + * environment variable contains exactly one "wrapper" -- a line + * of the form + * + * FILENAME_PATTERN FLAG OPTS [ FLAG OPTS ...] + * + * This may disagree with the documentation, which states: + * + * `$CVSWRAPPERS' + * A whitespace-separated list of file name patterns that CVS + * should treat as wrappers. *Note Wrappers::. + * + * Does this mean the environment variable can hold multiple + * wrappers lines? If so, a single call to wrap_add() is + * insufficient. + */ + + /* Then add entries found in CVSWRAPPERS environment variable. */ + wrap_add (getenv (WRAPPER_ENV), 0); +} + +#ifdef CLIENT_SUPPORT +/* Send -W arguments for the wrappers to the server. The command must + be one that accepts them (e.g. update, import). */ +void +wrap_send () +{ + int i; + + for (i = 0; i < wrap_count + wrap_tempcount; ++i) + { + if (wrap_list[i]->tocvsFilter != NULL + || wrap_list[i]->fromcvsFilter != NULL) + /* For greater studliness we would print the offending option + and (more importantly) where we found it. */ + error (0, 0, "\ +-t and -f wrapper options are not supported remotely; ignored"); + if (wrap_list[i]->mergeMethod == WRAP_COPY) + /* For greater studliness we would print the offending option + and (more importantly) where we found it. */ + error (0, 0, "\ +-m wrapper option is not supported remotely; ignored"); + if (wrap_list[i]->rcsOption != NULL) + { + send_to_server ("Argument -W\012Argument ", 0); + send_to_server (wrap_list[i]->wildCard, 0); + send_to_server (" -k '", 0); + send_to_server (wrap_list[i]->rcsOption, 0); + send_to_server ("'\012", 0); + } + } +} +#endif /* CLIENT_SUPPORT */ + +#if defined(SERVER_SUPPORT) || defined(CLIENT_SUPPORT) +/* Output wrapper entries in the format of cvswrappers lines. + * + * This is useful when one side of a client/server connection wants to + * send its wrappers to the other; since the receiving side would like + * to use wrap_add() to incorporate the wrapper, it's best if the + * entry arrives in this format. + * + * The entries are stored in `line', which is allocated here. Caller + * can free() it. + * + * If first_call_p is nonzero, then start afresh. */ +void +wrap_unparse_rcs_options (line, first_call_p) + char **line; + int first_call_p; +{ + /* FIXME-reentrancy: we should design a reentrant interface, like + a callback which gets handed each wrapper (a multithreaded + server being the most concrete reason for this, but the + non-reentrant interface is fairly unnecessary/ugly). */ + static int i; + + if (first_call_p) + i = 0; + + for (; i < wrap_count + wrap_tempcount; ++i) + { + if (wrap_list[i]->rcsOption != NULL) + { + *line = xmalloc (strlen (wrap_list[i]->wildCard) + + strlen ("\t") + + strlen (" -k '") + + strlen (wrap_list[i]->rcsOption) + + strlen ("'") + + 1); /* leave room for '\0' */ + + strcpy (*line, wrap_list[i]->wildCard); + strcat (*line, " -k '"); + strcat (*line, wrap_list[i]->rcsOption); + strcat (*line, "'"); + + /* We're going to miss the increment because we return, so + do it by hand. */ + ++i; + + return; + } + } + + *line = NULL; + return; +} +#endif /* SERVER_SUPPORT || CLIENT_SUPPORT */ + +/* + * Open a file and read lines, feeding each line to a line parser. Arrange + * for keeping a temporary list of wrappers at the end, if the "temp" + * argument is set. + */ +void +wrap_add_file (file, temp) + const char *file; + int temp; +{ + FILE *fp; + char *line = NULL; + size_t line_allocated = 0; + + wrap_restore_saved (); + wrap_kill_temp (); + + /* Load the file. */ + fp = CVS_FOPEN (file, "r"); + if (fp == NULL) + { + if (!existence_error (errno)) + error (0, errno, "cannot open %s", file); + return; + } + while (getline (&line, &line_allocated, fp) >= 0) + wrap_add (line, temp); + if (line) + free (line); + if (ferror (fp)) + error (0, errno, "cannot read %s", file); + if (fclose (fp) == EOF) + error (0, errno, "cannot close %s", file); +} + +void +wrap_kill() +{ + wrap_kill_temp(); + while(wrap_count) + wrap_free_entry(wrap_list[--wrap_count]); +} + +void +wrap_kill_temp() +{ + WrapperEntry **temps=wrap_list+wrap_count; + + while(wrap_tempcount) + wrap_free_entry(temps[--wrap_tempcount]); +} + +void +wrap_free_entry(e) + WrapperEntry *e; +{ + wrap_free_entry_internal(e); + free(e); +} + +void +wrap_free_entry_internal(e) + WrapperEntry *e; +{ + free (e->wildCard); + if (e->tocvsFilter) + free (e->tocvsFilter); + if (e->fromcvsFilter) + free (e->fromcvsFilter); + if (e->rcsOption) + free (e->rcsOption); +} + +void +wrap_restore_saved() +{ + if(!wrap_saved_list) + return; + + wrap_kill(); + + free(wrap_list); + + wrap_list=wrap_saved_list; + wrap_count=wrap_saved_count; + wrap_tempcount=wrap_saved_tempcount; + + wrap_saved_list=NULL; + wrap_saved_count=0; + wrap_saved_tempcount=0; +} + +void +wrap_add (line, isTemp) + char *line; + int isTemp; +{ + char *temp; + char ctemp; + WrapperEntry e; + char opt; + + if (!line || line[0] == '#') + return; + + memset (&e, 0, sizeof(e)); + + /* Search for the wild card */ + while (*line && isspace ((unsigned char) *line)) + ++line; + for (temp = line; + *line && !isspace ((unsigned char) *line); + ++line) + ; + if(temp==line) + return; + + ctemp=*line; + *line='\0'; + + e.wildCard=xstrdup(temp); + *line=ctemp; + + while(*line){ + /* Search for the option */ + while(*line && *line!='-') + ++line; + if(!*line) + break; + ++line; + if(!*line) + break; + opt=*line; + + /* Search for the filter commandline */ + for(++line;*line && *line!='\'';++line); + if(!*line) + break; + + for(temp=++line;*line && (*line!='\'' || line[-1]=='\\');++line) + ; + + /* This used to "break;" (ignore the option) if there was a + single character between the single quotes (I'm guessing + that was accidental). Now it "break;"s if there are no + characters. I'm not sure either behavior is particularly + necessary--the current options might not require '' + arguments, but surely some future option legitimately + might. Also I'm not sure that ignoring the option is a + swift way to handle syntax errors in general. */ + if (line==temp) + break; + + ctemp=*line; + *line='\0'; + switch(opt){ + case 'f': + /* Before this is reenabled, need to address the problem in + commit.c (see http://www.cyclic.com/cvs/dev-wrap.txt). */ + error (1, 0, + "-t/-f wrappers not supported by this version of CVS"); + + if(e.fromcvsFilter) + free(e.fromcvsFilter); + /* FIXME: error message should say where the bad value + came from. */ + e.fromcvsFilter=expand_path (temp, "<wrapper>", 0); + if (!e.fromcvsFilter) + error (1, 0, "Correct above errors first"); + break; + case 't': + /* Before this is reenabled, need to address the problem in + commit.c (see http://www.cyclic.com/cvs/dev-wrap.txt). */ + error (1, 0, + "-t/-f wrappers not supported by this version of CVS"); + + if(e.tocvsFilter) + free(e.tocvsFilter); + /* FIXME: error message should say where the bad value + came from. */ + e.tocvsFilter=expand_path (temp, "<wrapper>", 0); + if (!e.tocvsFilter) + error (1, 0, "Correct above errors first"); + break; + case 'm': + if(*temp=='C' || *temp=='c') + e.mergeMethod=WRAP_COPY; + else + e.mergeMethod=WRAP_MERGE; + break; + case 'k': + if (e.rcsOption) + free (e.rcsOption); + e.rcsOption = xstrdup (temp); + break; + default: + break; + } + *line=ctemp; + if(!*line)break; + ++line; + } + + wrap_add_entry(&e, isTemp); +} + +void +wrap_add_entry(e, temp) + WrapperEntry *e; + int temp; +{ + int x; + if(wrap_count+wrap_tempcount>=wrap_size){ + wrap_size += WRAPPER_GROW; + wrap_list = (WrapperEntry **) xrealloc ((char *) wrap_list, + wrap_size * + sizeof (WrapperEntry *)); + } + + if(!temp && wrap_tempcount){ + for(x=wrap_count+wrap_tempcount-1;x>=wrap_count;--x) + wrap_list[x+1]=wrap_list[x]; + } + + x=(temp ? wrap_count+(wrap_tempcount++):(wrap_count++)); + wrap_list[x]=(WrapperEntry *)xmalloc(sizeof(WrapperEntry)); + wrap_list[x]->wildCard=e->wildCard; + wrap_list[x]->fromcvsFilter=e->fromcvsFilter; + wrap_list[x]->tocvsFilter=e->tocvsFilter; + wrap_list[x]->mergeMethod=e->mergeMethod; + wrap_list[x]->rcsOption = e->rcsOption; +} + +/* Return 1 if the given filename is a wrapper filename */ +int +wrap_name_has (name,has) + const char *name; + WrapMergeHas has; +{ + int x,count=wrap_count+wrap_tempcount; + char *temp; + + for(x=0;x<count;++x) + if (CVS_FNMATCH (wrap_list[x]->wildCard, name, 0) == 0){ + switch(has){ + case WRAP_TOCVS: + temp=wrap_list[x]->tocvsFilter; + break; + case WRAP_FROMCVS: + temp=wrap_list[x]->fromcvsFilter; + break; + case WRAP_RCSOPTION: + temp = wrap_list[x]->rcsOption; + break; + default: + abort (); + } + if(temp==NULL) + return (0); + else + return (1); + } + return (0); +} + +static WrapperEntry *wrap_matching_entry PROTO ((const char *)); + +static WrapperEntry * +wrap_matching_entry (name) + const char *name; +{ + int x,count=wrap_count+wrap_tempcount; + + for(x=0;x<count;++x) + if (CVS_FNMATCH (wrap_list[x]->wildCard, name, 0) == 0) + return wrap_list[x]; + return (WrapperEntry *)NULL; +} + +/* Return the RCS options for FILENAME in a newly malloc'd string. If + ASFLAG, then include "-k" at the beginning (e.g. "-kb"), otherwise + just give the option itself (e.g. "b"). */ +char * +wrap_rcsoption (filename, asflag) + const char *filename; + int asflag; +{ + WrapperEntry *e = wrap_matching_entry (filename); + char *buf; + + if (e == NULL || e->rcsOption == NULL || (*e->rcsOption == '\0')) + return NULL; + + buf = xmalloc (strlen (e->rcsOption) + 3); + if (asflag) + { + strcpy (buf, "-k"); + strcat (buf, e->rcsOption); + } + else + { + strcpy (buf, e->rcsOption); + } + return buf; +} + +char * +wrap_tocvs_process_file(fileName) + const char *fileName; +{ + WrapperEntry *e=wrap_matching_entry(fileName); + static char *buf = NULL; + char *args; + + if(e==NULL || e->tocvsFilter==NULL) + return NULL; + + if (buf != NULL) + free (buf); + buf = cvs_temp_name (); + + args = xmalloc (strlen (e->tocvsFilter) + + strlen (fileName) + + strlen (buf)); + /* FIXME: sprintf will blow up if the format string contains items other + than %s, or contains too many %s's. We should instead be parsing + e->tocvsFilter ourselves and giving a real error. */ + sprintf (args, e->tocvsFilter, fileName, buf); + run_setup (args); + run_exec(RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL|RUN_REALLY ); + free (args); + + return buf; +} + +int +wrap_merge_is_copy (fileName) + const char *fileName; +{ + WrapperEntry *e=wrap_matching_entry(fileName); + if(e==NULL || e->mergeMethod==WRAP_MERGE) + return 0; + + return 1; +} + +void +wrap_fromcvs_process_file(fileName) + const char *fileName; +{ + char *args; + WrapperEntry *e=wrap_matching_entry(fileName); + + if(e==NULL || e->fromcvsFilter==NULL) + return; + + args = xmalloc (strlen (e->fromcvsFilter) + + strlen (fileName)); + /* FIXME: sprintf will blow up if the format string contains items other + than %s, or contains too many %s's. We should instead be parsing + e->fromcvsFilter ourselves and giving a real error. */ + sprintf (args, e->fromcvsFilter, fileName); + run_setup (args); + run_exec(RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL ); + free (args); + return; +} diff --git a/gnu/dist/cvs/src/zlib.c b/gnu/dist/cvs/src/zlib.c new file mode 100644 index 00000000000..fa0c2ad40d0 --- /dev/null +++ b/gnu/dist/cvs/src/zlib.c @@ -0,0 +1,662 @@ +/* zlib.c --- interface to the zlib compression library + Ian Lance Taylor <ian@cygnus.com> + + This file is part of GNU CVS. + + GNU CVS 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, 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. */ + +/* The routines in this file are the interface between the CVS + client/server support and the zlib compression library. */ + +#include <assert.h> +#include "cvs.h" +#include "buffer.h" + +#if defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT) + +#include "zlib.h" + +/* OS/2 doesn't have EIO. FIXME: this whole notion of turning + a different error into EIO strikes me as pretty dubious. */ +#if !defined (EIO) +#define EIO EBADPOS +#endif + +/* The compression interface is built upon the buffer data structure. + We provide a buffer type which compresses or decompresses the data + which passes through it. An input buffer decompresses the data + read from an underlying buffer, and an output buffer compresses the + data before writing it to an underlying buffer. */ + +/* This structure is the closure field of the buffer. */ + +struct compress_buffer +{ + /* The underlying buffer. */ + struct buffer *buf; + /* The compression information. */ + z_stream zstr; +}; + +static void compress_error PROTO((int, int, z_stream *, const char *)); +static int compress_buffer_input PROTO((void *, char *, int, int, int *)); +static int compress_buffer_output PROTO((void *, const char *, int, int *)); +static int compress_buffer_flush PROTO((void *)); +static int compress_buffer_block PROTO((void *, int)); +static int compress_buffer_shutdown_input PROTO((void *)); +static int compress_buffer_shutdown_output PROTO((void *)); + +/* Report an error from one of the zlib functions. */ + +static void +compress_error (status, zstatus, zstr, msg) + int status; + int zstatus; + z_stream *zstr; + const char *msg; +{ + int hold_errno; + const char *zmsg; + char buf[100]; + + hold_errno = errno; + + zmsg = zstr->msg; + if (zmsg == NULL) + { + sprintf (buf, "error %d", zstatus); + zmsg = buf; + } + + error (status, + zstatus == Z_ERRNO ? hold_errno : 0, + "%s: %s", msg, zmsg); +} + +/* Create a compression buffer. */ + +struct buffer * +compress_buffer_initialize (buf, input, level, memory) + struct buffer *buf; + int input; + int level; + void (*memory) PROTO((struct buffer *)); +{ + struct compress_buffer *n; + int zstatus; + + n = (struct compress_buffer *) xmalloc (sizeof *n); + memset (n, 0, sizeof *n); + + n->buf = buf; + + if (input) + zstatus = inflateInit (&n->zstr); + else + zstatus = deflateInit (&n->zstr, level); + if (zstatus != Z_OK) + compress_error (1, zstatus, &n->zstr, "compression initialization"); + + /* There may already be data buffered on BUF. For an output + buffer, this is OK, because these routines will just use the + buffer routines to append data to the (uncompressed) data + already on BUF. An input buffer expects to handle a single + buffer_data of buffered input to be uncompressed, so that is OK + provided there is only one buffer. At present that is all + there ever will be; if this changes, compress_buffer_input must + be modified to handle multiple input buffers. */ + assert (! input || buf->data == NULL || buf->data->next == NULL); + + return buf_initialize (input ? compress_buffer_input : NULL, + input ? NULL : compress_buffer_output, + input ? NULL : compress_buffer_flush, + compress_buffer_block, + (input + ? compress_buffer_shutdown_input + : compress_buffer_shutdown_output), + memory, + n); +} + +/* Input data from a compression buffer. */ + +static int +compress_buffer_input (closure, data, need, size, got) + void *closure; + char *data; + int need; + int size; + int *got; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + struct buffer_data *bd; + + if (cb->buf->input == NULL) + abort (); + + /* We use a single buffer_data structure to buffer up data which + the z_stream structure won't use yet. We can safely store this + on cb->buf->data, because we never call the buffer routines on + cb->buf; we only call the buffer input routine, since that + gives us the semantics we want. As noted in + compress_buffer_initialize, the buffer_data structure may + already exist, and hold data which was already read and + buffered before the decompression began. */ + bd = cb->buf->data; + if (bd == NULL) + { + bd = ((struct buffer_data *) malloc (sizeof (struct buffer_data))); + if (bd == NULL) + return -2; + bd->text = (char *) malloc (BUFFER_DATA_SIZE); + if (bd->text == NULL) + { + free (bd); + return -2; + } + bd->bufp = bd->text; + bd->size = 0; + cb->buf->data = bd; + } + + cb->zstr.avail_out = size; + cb->zstr.next_out = (Bytef *) data; + + while (1) + { + int zstatus, sofar, status, nread; + + /* First try to inflate any data we already have buffered up. + This is useful even if we don't have any buffered data, + because there may be data buffered inside the z_stream + structure. */ + + cb->zstr.avail_in = bd->size; + cb->zstr.next_in = (Bytef *) bd->bufp; + + do + { + zstatus = inflate (&cb->zstr, Z_NO_FLUSH); + if (zstatus == Z_STREAM_END) + break; + if (zstatus != Z_OK && zstatus != Z_BUF_ERROR) + { + compress_error (0, zstatus, &cb->zstr, "inflate"); + return EIO; + } + } while (cb->zstr.avail_in > 0 + && cb->zstr.avail_out > 0); + + bd->size = cb->zstr.avail_in; + bd->bufp = (char *) cb->zstr.next_in; + + if (zstatus == Z_STREAM_END) + return -1; + + /* If we have obtained NEED bytes, then return, unless NEED is + zero and we haven't obtained anything at all. If NEED is + zero, we will keep reading from the underlying buffer until + we either can't read anything, or we have managed to + inflate at least one byte. */ + sofar = size - cb->zstr.avail_out; + if (sofar > 0 && sofar >= need) + break; + + /* All our buffered data should have been processed at this + point. */ + assert (bd->size == 0); + + /* This will work well in the server, because this call will + do an unblocked read and fetch all the available data. In + the client, this will read a single byte from the stdio + stream, which will cause us to call inflate once per byte. + It would be more efficient if we could make a call which + would fetch all the available bytes, and at least one byte. */ + + status = (*cb->buf->input) (cb->buf->closure, bd->text, + need > 0 ? 1 : 0, + BUFFER_DATA_SIZE, &nread); + if (status != 0) + return status; + + /* If we didn't read anything, then presumably the buffer is + in nonblocking mode, and we should just get out now with + whatever we've inflated. */ + if (nread == 0) + { + assert (need == 0); + break; + } + + bd->bufp = bd->text; + bd->size = nread; + } + + *got = size - cb->zstr.avail_out; + + return 0; +} + +/* Output data to a compression buffer. */ + +static int +compress_buffer_output (closure, data, have, wrote) + void *closure; + const char *data; + int have; + int *wrote; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + + cb->zstr.avail_in = have; + cb->zstr.next_in = (unsigned char *) data; + + while (cb->zstr.avail_in > 0) + { + char buffer[BUFFER_DATA_SIZE]; + int zstatus; + + cb->zstr.avail_out = BUFFER_DATA_SIZE; + cb->zstr.next_out = (unsigned char *) buffer; + + zstatus = deflate (&cb->zstr, Z_NO_FLUSH); + if (zstatus != Z_OK) + { + compress_error (0, zstatus, &cb->zstr, "deflate"); + return EIO; + } + + if (cb->zstr.avail_out != BUFFER_DATA_SIZE) + buf_output (cb->buf, buffer, + BUFFER_DATA_SIZE - cb->zstr.avail_out); + } + + *wrote = have; + + /* We will only be here because buf_send_output was called on the + compression buffer. That means that we should now call + buf_send_output on the underlying buffer. */ + return buf_send_output (cb->buf); +} + +/* Flush a compression buffer. */ + +static int +compress_buffer_flush (closure) + void *closure; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + + cb->zstr.avail_in = 0; + cb->zstr.next_in = NULL; + + while (1) + { + char buffer[BUFFER_DATA_SIZE]; + int zstatus; + + cb->zstr.avail_out = BUFFER_DATA_SIZE; + cb->zstr.next_out = (unsigned char *) buffer; + + zstatus = deflate (&cb->zstr, Z_SYNC_FLUSH); + + /* The deflate function will return Z_BUF_ERROR if it can't do + anything, which in this case means that all data has been + flushed. */ + if (zstatus == Z_BUF_ERROR) + break; + + if (zstatus != Z_OK) + { + compress_error (0, zstatus, &cb->zstr, "deflate flush"); + return EIO; + } + + if (cb->zstr.avail_out != BUFFER_DATA_SIZE) + buf_output (cb->buf, buffer, + BUFFER_DATA_SIZE - cb->zstr.avail_out); + + /* If the deflate function did not fill the output buffer, + then all data has been flushed. */ + if (cb->zstr.avail_out > 0) + break; + } + + /* Now flush the underlying buffer. Note that if the original + call to buf_flush passed 1 for the BLOCK argument, then the + buffer will already have been set into blocking mode, so we + should always pass 0 here. */ + return buf_flush (cb->buf, 0); +} + +/* The block routine for a compression buffer. */ + +static int +compress_buffer_block (closure, block) + void *closure; + int block; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + + if (block) + return set_block (cb->buf); + else + return set_nonblock (cb->buf); +} + +/* Shut down an input buffer. */ + +static int +compress_buffer_shutdown_input (closure) + void *closure; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + int zstatus; + + /* Pick up any trailing data, such as the checksum. */ + while (1) + { + int status, nread; + char buf[100]; + + status = compress_buffer_input (cb, buf, 0, sizeof buf, &nread); + if (status == -1) + break; + if (status != 0) + return status; + } + + zstatus = inflateEnd (&cb->zstr); + if (zstatus != Z_OK) + { + compress_error (0, zstatus, &cb->zstr, "inflateEnd"); + return EIO; + } + + return buf_shutdown (cb->buf); +} + +/* Shut down an output buffer. */ + +static int +compress_buffer_shutdown_output (closure) + void *closure; +{ + struct compress_buffer *cb = (struct compress_buffer *) closure; + int zstatus, status; + + do + { + char buffer[BUFFER_DATA_SIZE]; + + cb->zstr.avail_out = BUFFER_DATA_SIZE; + cb->zstr.next_out = (unsigned char *) buffer; + + zstatus = deflate (&cb->zstr, Z_FINISH); + if (zstatus != Z_OK && zstatus != Z_STREAM_END) + { + compress_error (0, zstatus, &cb->zstr, "deflate finish"); + return EIO; + } + + if (cb->zstr.avail_out != BUFFER_DATA_SIZE) + buf_output (cb->buf, buffer, + BUFFER_DATA_SIZE - cb->zstr.avail_out); + } while (zstatus != Z_STREAM_END); + + zstatus = deflateEnd (&cb->zstr); + if (zstatus != Z_OK) + { + compress_error (0, zstatus, &cb->zstr, "deflateEnd"); + return EIO; + } + + status = buf_flush (cb->buf, 1); + if (status != 0) + return status; + + return buf_shutdown (cb->buf); +} + + + +/* Here is our librarified gzip implementation. It is very minimal + but attempts to be RFC1952 compliant. */ + +/* BUF should contain SIZE bytes of gzipped data (RFC1952/RFC1951). + We are to uncompress the data and write the result to the file + descriptor FD. If something goes wrong, give a nonfatal error message + mentioning FULLNAME as the name of the file for FD. Return 1 if + it is an error we can't recover from. */ + +int +gunzip_and_write (fd, fullname, buf, size) + int fd; + char *fullname; + unsigned char *buf; + size_t size; +{ + size_t pos; + z_stream zstr; + int zstatus; + unsigned char outbuf[32768]; + unsigned long crc; + + if (buf[0] != 31 || buf[1] != 139) + { + error (0, 0, "gzipped data does not start with gzip identification"); + return 1; + } + if (buf[2] != 8) + { + error (0, 0, "only the deflate compression method is supported"); + return 1; + } + + /* Skip over the fixed header, and then skip any of the variable-length + fields. */ + pos = 10; + if (buf[3] & 4) + pos += buf[pos] + (buf[pos + 1] << 8) + 2; + if (buf[3] & 8) + pos += strlen (buf + pos) + 1; + if (buf[3] & 16) + pos += strlen (buf + pos) + 1; + if (buf[3] & 2) + pos += 2; + + memset (&zstr, 0, sizeof zstr); + /* Passing a negative argument tells zlib not to look for a zlib + (RFC1950) header. This is an undocumented feature; I suppose if + we wanted to be anal we could synthesize a header instead, + but why bother? */ + zstatus = inflateInit2 (&zstr, -15); + + if (zstatus != Z_OK) + compress_error (1, zstatus, &zstr, fullname); + + /* I don't see why we should have to include the 8 byte trailer in + avail_in. But I see that zlib/gzio.c does, and it seemed to fix + a fairly rare bug in which we'd get a Z_BUF_ERROR for no obvious + reason. */ + zstr.avail_in = size - pos; + zstr.next_in = buf + pos; + + crc = crc32 (0, NULL, 0); + + do + { + zstr.avail_out = sizeof (outbuf); + zstr.next_out = outbuf; + zstatus = inflate (&zstr, Z_NO_FLUSH); + if (zstatus != Z_STREAM_END && zstatus != Z_OK) + { + compress_error (0, zstatus, &zstr, fullname); + return 1; + } + if (write (fd, outbuf, sizeof (outbuf) - zstr.avail_out) < 0) + { + error (0, errno, "writing decompressed file %s", fullname); + return 1; + } + crc = crc32 (crc, outbuf, sizeof (outbuf) - zstr.avail_out); + } while (zstatus != Z_STREAM_END); + zstatus = inflateEnd (&zstr); + if (zstatus != Z_OK) + compress_error (0, zstatus, &zstr, fullname); + + if (crc != (buf[zstr.total_in + 10] + + (buf[zstr.total_in + 11] << 8) + + (buf[zstr.total_in + 12] << 16) + + (buf[zstr.total_in + 13] << 24))) + { + error (0, 0, "CRC error uncompressing %s", fullname); + return 1; + } + + if (zstr.total_out != (buf[zstr.total_in + 14] + + (buf[zstr.total_in + 15] << 8) + + (buf[zstr.total_in + 16] << 16) + + (buf[zstr.total_in + 17] << 24))) + { + error (0, 0, "invalid length uncompressing %s", fullname); + return 1; + } + + return 0; +} + +/* Read all of FD and put the gzipped data (RFC1952/RFC1951) into *BUF, + replacing previous contents of *BUF. *BUF is malloc'd and *SIZE is + its allocated size. Put the actual number of bytes of data in + *LEN. If something goes wrong, give a nonfatal error mentioning + FULLNAME as the name of the file for FD, and return 1 if we can't + recover from it). LEVEL is the compression level (1-9). */ + +int +read_and_gzip (fd, fullname, buf, size, len, level) + int fd; + char *fullname; + unsigned char **buf; + size_t *size; + size_t *len; + int level; +{ + z_stream zstr; + int zstatus; + unsigned char inbuf[8192]; + int nread; + unsigned long crc; + + if (*size < 1024) + { + unsigned char *newbuf; + + *size = 1024; + newbuf = realloc (*buf, *size); + if (newbuf == NULL) + { + error (0, 0, "out of memory"); + return 1; + } + *buf = newbuf; + } + (*buf)[0] = 31; + (*buf)[1] = 139; + (*buf)[2] = 8; + (*buf)[3] = 0; + (*buf)[4] = (*buf)[5] = (*buf)[6] = (*buf)[7] = 0; + /* Could set this based on level, but why bother? */ + (*buf)[8] = 0; + (*buf)[9] = 255; + + memset (&zstr, 0, sizeof zstr); + zstatus = deflateInit2 (&zstr, level, Z_DEFLATED, -15, 8, + Z_DEFAULT_STRATEGY); + crc = crc32 (0, NULL, 0); + if (zstatus != Z_OK) + { + compress_error (0, zstatus, &zstr, fullname); + return 1; + } + zstr.avail_out = *size; + zstr.next_out = *buf + 10; + + while (1) + { + int finish = 0; + + nread = read (fd, inbuf, sizeof inbuf); + if (nread < 0) + { + error (0, errno, "cannot read %s", fullname); + return 1; + } + else if (nread == 0) + /* End of file. */ + finish = 1; + crc = crc32 (crc, inbuf, nread); + zstr.next_in = inbuf; + zstr.avail_in = nread; + + do + { + size_t offset; + + /* I don't see this documented anywhere, but deflate seems + to tend to dump core sometimes if we pass it Z_FINISH and + a small (e.g. 2147 byte) avail_out. So we insist on at + least 4096 bytes (that is what zlib/gzio.c uses). */ + + if (zstr.avail_out < 4096) + { + unsigned char *newbuf; + + offset = zstr.next_out - *buf; + *size *= 2; + newbuf = realloc (*buf, *size); + if (newbuf == NULL) + { + error (0, 0, "out of memory"); + return 1; + } + *buf = newbuf; + zstr.next_out = *buf + offset; + zstr.avail_out = *size - offset; + } + + zstatus = deflate (&zstr, finish ? Z_FINISH : 0); + if (zstatus == Z_STREAM_END) + goto done; + else if (zstatus != Z_OK) + compress_error (0, zstatus, &zstr, fullname); + } while (zstr.avail_out == 0); + } + done: + *(*buf + zstr.total_out + 10) = crc & 0xff; + *(*buf + zstr.total_out + 11) = (crc >> 8) & 0xff; + *(*buf + zstr.total_out + 12) = (crc >> 16) & 0xff; + *(*buf + zstr.total_out + 13) = (crc >> 24) & 0xff; + + *(*buf + zstr.total_out + 14) = zstr.total_in & 0xff; + *(*buf + zstr.total_out + 15) = (zstr.total_in >> 8) & 0xff; + *(*buf + zstr.total_out + 16) = (zstr.total_in >> 16) & 0xff; + *(*buf + zstr.total_out + 17) = (zstr.total_in >> 24) & 0xff; + + *len = zstr.total_out + 18; + + zstatus = deflateEnd (&zstr); + if (zstatus != Z_OK) + compress_error (0, zstatus, &zstr, fullname); + + return 0; +} +#endif /* defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT) */ |
