logmsg.c revision 107487
1/*
2 * Copyright (c) 1992, Brian Berliner and Jeff Polk
3 * Copyright (c) 1989-1992, Brian Berliner
4 *
5 * You may distribute under the terms of the GNU General Public License as
6 * specified in the README file that comes with the CVS source distribution.
7 *
8 * $FreeBSD: head/contrib/cvs/src/logmsg.c 107487 2002-12-02 03:17:49Z peter $
9 */
10
11#include <assert.h>
12
13#include "cvs.h"
14#include "getline.h"
15
16static int find_type PROTO((Node * p, void *closure));
17static int fmt_proc PROTO((Node * p, void *closure));
18static int logfile_write PROTO((char *repository, char *filter,
19			  char *message, FILE * logfp, List * changes));
20static int rcsinfo_proc PROTO((char *repository, char *template));
21static int title_proc PROTO((Node * p, void *closure));
22static int update_logfile_proc PROTO((char *repository, char *filter));
23static void setup_tmpfile PROTO((FILE * xfp, char *xprefix, List * changes));
24static int editinfo_proc PROTO((char *repository, char *template));
25static int verifymsg_proc PROTO((char *repository, char *script));
26
27static FILE *fp;
28static char *str_list;
29static char *str_list_format;	/* The format for str_list's contents. */
30static char *editinfo_editor;
31static char *verifymsg_script;
32static Ctype type;
33
34/*
35 * Should the logmsg be re-read during the do_verify phase?
36 * RereadLogAfterVerify=no|stat|yes
37 * LOGMSG_REREAD_NEVER  - never re-read the logmsg
38 * LOGMSG_REREAD_STAT   - re-read the logmsg only if it has changed
39 * LOGMSG_REREAD_ALWAYS - always re-read the logmsg
40 */
41int RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
42
43/*
44 * Puts a standard header on the output which is either being prepared for an
45 * editor session, or being sent to a logfile program.  The modified, added,
46 * and removed files are included (if any) and formatted to look pretty. */
47static char *prefix;
48static int col;
49static char *tag;
50static void
51setup_tmpfile (xfp, xprefix, changes)
52    FILE *xfp;
53    char *xprefix;
54    List *changes;
55{
56    /* set up statics */
57    fp = xfp;
58    prefix = xprefix;
59
60    type = T_MODIFIED;
61    if (walklist (changes, find_type, NULL) != 0)
62    {
63	(void) fprintf (fp, "%sModified Files:\n", prefix);
64	col = 0;
65	(void) walklist (changes, fmt_proc, NULL);
66	(void) fprintf (fp, "\n");
67	if (tag != NULL)
68	{
69	    free (tag);
70	    tag = NULL;
71	}
72    }
73    type = T_ADDED;
74    if (walklist (changes, find_type, NULL) != 0)
75    {
76	(void) fprintf (fp, "%sAdded Files:\n", prefix);
77	col = 0;
78	(void) walklist (changes, fmt_proc, NULL);
79	(void) fprintf (fp, "\n");
80	if (tag != NULL)
81	{
82	    free (tag);
83	    tag = NULL;
84	}
85    }
86    type = T_REMOVED;
87    if (walklist (changes, find_type, NULL) != 0)
88    {
89	(void) fprintf (fp, "%sRemoved Files:\n", prefix);
90	col = 0;
91	(void) walklist (changes, fmt_proc, NULL);
92	(void) fprintf (fp, "\n");
93	if (tag != NULL)
94	{
95	    free (tag);
96	    tag = NULL;
97	}
98    }
99}
100
101/*
102 * Looks for nodes of a specified type and returns 1 if found
103 */
104static int
105find_type (p, closure)
106    Node *p;
107    void *closure;
108{
109    struct logfile_info *li;
110
111    li = (struct logfile_info *) p->data;
112    if (li->type == type)
113	return (1);
114    else
115	return (0);
116}
117
118/*
119 * Breaks the files list into reasonable sized lines to avoid line wrap...
120 * all in the name of pretty output.  It only works on nodes whose types
121 * match the one we're looking for
122 */
123static int
124fmt_proc (p, closure)
125    Node *p;
126    void *closure;
127{
128    struct logfile_info *li;
129
130    li = (struct logfile_info *) p->data;
131    if (li->type == type)
132    {
133        if (li->tag == NULL
134	    ? tag != NULL
135	    : tag == NULL || strcmp (tag, li->tag) != 0)
136	{
137	    if (col > 0)
138	        (void) fprintf (fp, "\n");
139	    (void) fputs (prefix, fp);
140	    col = strlen (prefix);
141	    while (col < 6)
142	    {
143	        (void) fprintf (fp, " ");
144		++col;
145	    }
146
147	    if (li->tag == NULL)
148	        (void) fprintf (fp, "No tag");
149	    else
150	        (void) fprintf (fp, "Tag: %s", li->tag);
151
152	    if (tag != NULL)
153	        free (tag);
154	    tag = xstrdup (li->tag);
155
156	    /* Force a new line.  */
157	    col = 70;
158	}
159
160	if (col == 0)
161	{
162	    (void) fprintf (fp, "%s\t", prefix);
163	    col = 8;
164	}
165	else if (col > 8 && (col + (int) strlen (p->key)) > 70)
166	{
167	    (void) fprintf (fp, "\n%s\t", prefix);
168	    col = 8;
169	}
170	(void) fprintf (fp, "%s ", p->key);
171	col += strlen (p->key) + 1;
172    }
173    return (0);
174}
175
176/*
177 * Builds a temporary file using setup_tmpfile() and invokes the user's
178 * editor on the file.  The header garbage in the resultant file is then
179 * stripped and the log message is stored in the "message" argument.
180 *
181 * If REPOSITORY is non-NULL, process rcsinfo for that repository; if it
182 * is NULL, use the CVSADM_TEMPLATE file instead.  REPOSITORY should be
183 * NULL when running in client mode.
184 */
185void
186do_editor (dir, messagep, repository, changes)
187    char *dir;
188    char **messagep;
189    char *repository;
190    List *changes;
191{
192    static int reuse_log_message = 0;
193    char *line;
194    int line_length;
195    size_t line_chars_allocated;
196    char *fname;
197    struct stat pre_stbuf, post_stbuf;
198    int retcode = 0;
199
200#ifdef CLIENT_SUPPORT
201    assert (!current_parsed_root->isremote != !repository);
202#else
203    assert (repository);
204#endif
205
206    if (noexec || reuse_log_message)
207	return;
208
209    /* Abort creation of temp file if no editor is defined */
210    if (strcmp (Editor, "") == 0 && !editinfo_editor)
211	error(1, 0, "no editor defined, must use -e or -m");
212
213    /* Create a temporary file */
214    /* FIXME - It's possible we should be relying on cvs_temp_file to open
215     * the file here - we get race conditions otherwise.
216     */
217    fname = cvs_temp_name ();
218  again:
219    if ((fp = CVS_FOPEN (fname, "w+")) == NULL)
220	error (1, 0, "cannot create temporary file %s", fname);
221
222    if (*messagep)
223    {
224	(void) fputs (*messagep, fp);
225
226	if ((*messagep)[0] == '\0' ||
227	    (*messagep)[strlen (*messagep) - 1] != '\n')
228	    (void) fprintf (fp, "\n");
229    }
230    else
231	(void) fprintf (fp, "\n");
232
233    if (repository != NULL)
234	/* tack templates on if necessary */
235	(void) Parse_Info (CVSROOTADM_RCSINFO, repository, rcsinfo_proc, 1);
236    else
237    {
238	FILE *tfp;
239	char buf[1024];
240	size_t n;
241	size_t nwrite;
242
243	/* Why "b"?  */
244	tfp = CVS_FOPEN (CVSADM_TEMPLATE, "rb");
245	if (tfp == NULL)
246	{
247	    if (!existence_error (errno))
248		error (1, errno, "cannot read %s", CVSADM_TEMPLATE);
249	}
250	else
251	{
252	    while (!feof (tfp))
253	    {
254		char *p = buf;
255		n = fread (buf, 1, sizeof buf, tfp);
256		nwrite = n;
257		while (nwrite > 0)
258		{
259		    n = fwrite (p, 1, nwrite, fp);
260		    nwrite -= n;
261		    p += n;
262		}
263		if (ferror (tfp))
264		    error (1, errno, "cannot read %s", CVSADM_TEMPLATE);
265	    }
266	    if (fclose (tfp) < 0)
267		error (0, errno, "cannot close %s", CVSADM_TEMPLATE);
268	}
269    }
270
271    (void) fprintf (fp,
272  "%s----------------------------------------------------------------------\n",
273		    CVSEDITPREFIX);
274    (void) fprintf (fp,
275  "%sEnter Log.  Lines beginning with `%.*s' are removed automatically\n%s\n",
276		    CVSEDITPREFIX, CVSEDITPREFIXLEN, CVSEDITPREFIX,
277		    CVSEDITPREFIX);
278    if (dir != NULL && *dir)
279	(void) fprintf (fp, "%sCommitting in %s\n%s\n", CVSEDITPREFIX,
280			dir, CVSEDITPREFIX);
281    if (changes != NULL)
282	setup_tmpfile (fp, CVSEDITPREFIX, changes);
283    (void) fprintf (fp,
284  "%s----------------------------------------------------------------------\n",
285		    CVSEDITPREFIX);
286
287    /* finish off the temp file */
288    if (fclose (fp) == EOF)
289        error (1, errno, "%s", fname);
290    if ( CVS_STAT (fname, &pre_stbuf) == -1)
291	pre_stbuf.st_mtime = 0;
292
293    if (editinfo_editor)
294	free (editinfo_editor);
295    editinfo_editor = (char *) NULL;
296#ifdef CLIENT_SUPPORT
297    if (current_parsed_root->isremote)
298	; /* nothing, leave editinfo_editor NULL */
299    else
300#endif
301    if (repository != NULL)
302	(void) Parse_Info (CVSROOTADM_EDITINFO, repository, editinfo_proc, 0);
303
304    /* run the editor */
305    run_setup (editinfo_editor ? editinfo_editor : Editor);
306    run_arg (fname);
307    if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY,
308			     RUN_NORMAL | RUN_SIGIGNORE)) != 0)
309	error (editinfo_editor ? 1 : 0, retcode == -1 ? errno : 0,
310	       editinfo_editor ? "Logfile verification failed" :
311	       "warning: editor session failed");
312
313    /* put the entire message back into the *messagep variable */
314
315    fp = open_file (fname, "r");
316
317    if (*messagep)
318	free (*messagep);
319
320    if ( CVS_STAT (fname, &post_stbuf) != 0)
321	    error (1, errno, "cannot find size of temp file %s", fname);
322
323    if (post_stbuf.st_size == 0)
324	*messagep = NULL;
325    else
326    {
327	/* On NT, we might read less than st_size bytes, but we won't
328	   read more.  So this works.  */
329	*messagep = (char *) xmalloc (post_stbuf.st_size + 1);
330 	(*messagep)[0] = '\0';
331    }
332
333    line = NULL;
334    line_chars_allocated = 0;
335
336    if (*messagep)
337    {
338	size_t message_len = post_stbuf.st_size + 1;
339	size_t offset = 0;
340	while (1)
341	{
342	    line_length = getline (&line, &line_chars_allocated, fp);
343	    if (line_length == -1)
344	    {
345		if (ferror (fp))
346		    error (0, errno, "warning: cannot read %s", fname);
347		break;
348	    }
349	    if (strncmp (line, CVSEDITPREFIX, CVSEDITPREFIXLEN) == 0)
350		continue;
351	    if (offset + line_length >= message_len)
352		expand_string (messagep, &message_len,
353				offset + line_length + 1);
354	    (void) strcpy (*messagep + offset, line);
355	    offset += line_length;
356	}
357    }
358    if (fclose (fp) < 0)
359	error (0, errno, "warning: cannot close %s", fname);
360
361    /* canonicalize emply messages */
362    if (*messagep != NULL &&
363        (**messagep == '\0' || strcmp (*messagep, "\n") == 0))
364    {
365	free (*messagep);
366	*messagep = NULL;
367    }
368
369    if (pre_stbuf.st_mtime == post_stbuf.st_mtime || *messagep == NULL)
370    {
371	for (;;)
372	{
373	    (void) printf ("\nLog message unchanged or not specified\n");
374	    (void) printf ("a)bort, c)ontinue, e)dit, !)reuse this message unchanged for remaining dirs\n");
375	    (void) printf ("Action: (continue) ");
376	    (void) fflush (stdout);
377	    line_length = getline (&line, &line_chars_allocated, stdin);
378	    if (line_length < 0)
379	    {
380		error (0, errno, "cannot read from stdin");
381		if (unlink_file (fname) < 0)
382		    error (0, errno,
383			   "warning: cannot remove temp file %s", fname);
384		error (1, 0, "aborting");
385	    }
386	    else if (line_length == 0
387		     || *line == '\n' || *line == 'c' || *line == 'C')
388		break;
389	    if (*line == 'a' || *line == 'A')
390		{
391		    if (unlink_file (fname) < 0)
392			error (0, errno, "warning: cannot remove temp file %s", fname);
393		    error (1, 0, "aborted by user");
394		}
395	    if (*line == 'e' || *line == 'E')
396		goto again;
397	    if (*line == '!')
398	    {
399		reuse_log_message = 1;
400		break;
401	    }
402	    (void) printf ("Unknown input\n");
403	}
404    }
405    if (line)
406	free (line);
407    if (unlink_file (fname) < 0)
408	error (0, errno, "warning: cannot remove temp file %s", fname);
409    free (fname);
410}
411
412/* Runs the user-defined verification script as part of the commit or import
413   process.  This verification is meant to be run whether or not the user
414   included the -m atribute.  unlike the do_editor function, this is
415   independant of the running of an editor for getting a message.
416 */
417void
418do_verify (messagep, repository)
419    char **messagep;
420    char *repository;
421{
422    FILE *fp;
423    char *fname;
424    int retcode = 0;
425
426    struct stat pre_stbuf, post_stbuf;
427
428#ifdef CLIENT_SUPPORT
429    if (current_parsed_root->isremote)
430	/* The verification will happen on the server.  */
431	return;
432#endif
433
434    /* FIXME? Do we really want to skip this on noexec?  What do we do
435       for the other administrative files?  */
436    if (noexec)
437	return;
438
439    /* Get the name of the verification script to run  */
440
441    if (repository != NULL)
442	(void) Parse_Info (CVSROOTADM_VERIFYMSG, repository,
443			   verifymsg_proc, 0);
444    if (!verifymsg_script)
445	return;
446
447    /* open a temporary file, write the message to the
448       temp file, and close the file.  */
449
450    if ((fp = cvs_temp_file (&fname)) == NULL)
451	error (1, errno, "cannot create temporary file %s", fname);
452
453    if (*messagep != NULL)
454	fputs (*messagep, fp);
455    if (*messagep == NULL ||
456	(*messagep)[0] == '\0' ||
457	(*messagep)[strlen (*messagep) - 1] != '\n')
458	putc ('\n', fp);
459    if (fclose (fp) == EOF)
460	error (1, errno, "%s", fname);
461
462    if (RereadLogAfterVerify == LOGMSG_REREAD_STAT)
463    {
464	/* Remember the status of the temp file for later */
465	if ( CVS_STAT (fname, &pre_stbuf) != 0 )
466	    error (1, errno, "cannot stat temp file %s", fname);
467
468	/*
469	 * See if we need to sleep before running the verification
470	 * script to avoid time-stamp races.
471	 */
472	sleep_past (pre_stbuf.st_mtime);
473    }
474
475    run_setup (verifymsg_script);
476    run_arg (fname);
477    if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY,
478			     RUN_NORMAL | RUN_SIGIGNORE)) != 0)
479    {
480	/* Since following error() exits, delete the temp file now.  */
481	if (unlink_file (fname) < 0)
482	    error (0, errno, "cannot remove %s", fname);
483
484	error (1, retcode == -1 ? errno : 0,
485	       "Message verification failed");
486    }
487
488    /* Get the mod time and size of the possibly new log message
489     * in always and stat modes.
490     */
491    if (RereadLogAfterVerify == LOGMSG_REREAD_ALWAYS ||
492	RereadLogAfterVerify == LOGMSG_REREAD_STAT)
493    {
494	if ( CVS_STAT (fname, &post_stbuf) != 0 )
495	    error (1, errno, "cannot find size of temp file %s", fname);
496    }
497
498    /* And reread the log message in `always' mode or in `stat' mode when it's
499     * changed
500     */
501    if (RereadLogAfterVerify == LOGMSG_REREAD_ALWAYS ||
502	(RereadLogAfterVerify == LOGMSG_REREAD_STAT &&
503	    (pre_stbuf.st_mtime != post_stbuf.st_mtime ||
504	     pre_stbuf.st_size != post_stbuf.st_size)))
505    {
506	/* put the entire message back into the *messagep variable */
507
508	if (*messagep) free (*messagep);
509
510	if (post_stbuf.st_size == 0)
511	    *messagep = NULL;
512	else
513	{
514	    char *line = NULL;
515	    int line_length;
516	    size_t line_chars_allocated = 0;
517	    char *p;
518
519	    if ( (fp = open_file (fname, "r")) == NULL )
520		error (1, errno, "cannot open temporary file %s", fname);
521
522	    /* On NT, we might read less than st_size bytes,
523	       but we won't read more.  So this works.  */
524	    p = *messagep = (char *) xmalloc (post_stbuf.st_size + 1);
525	    *messagep[0] = '\0';
526
527	    while (1)
528	    {
529		line_length = getline (&line,
530				       &line_chars_allocated,
531				       fp);
532		if (line_length == -1)
533		{
534		    if (ferror (fp))
535			/* Fail in this case because otherwise we will have no
536			 * log message
537			 */
538			error (1, errno, "cannot read %s", fname);
539		    break;
540		}
541		if (strncmp (line, CVSEDITPREFIX, CVSEDITPREFIXLEN) == 0)
542		    continue;
543		(void) strcpy (p, line);
544		p += line_length;
545	    }
546	    if (line) free (line);
547	    if (fclose (fp) < 0)
548	        error (0, errno, "warning: cannot close %s", fname);
549	}
550    }
551
552    /* Delete the temp file  */
553
554    if (unlink_file (fname) < 0)
555	error (0, errno, "cannot remove %s", fname);
556    free (fname);
557}
558
559/*
560 * callback proc for Parse_Info for rcsinfo templates this routine basically
561 * copies the matching template onto the end of the tempfile we are setting
562 * up
563 */
564/* ARGSUSED */
565static int
566rcsinfo_proc (repository, template)
567    char *repository;
568    char *template;
569{
570    static char *last_template;
571    FILE *tfp;
572
573    /* nothing to do if the last one included is the same as this one */
574    if (last_template && strcmp (last_template, template) == 0)
575	return (0);
576    if (last_template)
577	free (last_template);
578    last_template = xstrdup (template);
579
580    if ((tfp = CVS_FOPEN (template, "r")) != NULL)
581    {
582	char *line = NULL;
583	size_t line_chars_allocated = 0;
584
585	while (getline (&line, &line_chars_allocated, tfp) >= 0)
586	    (void) fputs (line, fp);
587	if (ferror (tfp))
588	    error (0, errno, "warning: cannot read %s", template);
589	if (fclose (tfp) < 0)
590	    error (0, errno, "warning: cannot close %s", template);
591	if (line)
592	    free (line);
593	return (0);
594    }
595    else
596    {
597	error (0, errno, "Couldn't open rcsinfo template file %s", template);
598	return (1);
599    }
600}
601
602/*
603 * Uses setup_tmpfile() to pass the updated message on directly to any
604 * logfile programs that have a regular expression match for the checked in
605 * directory in the source repository.  The log information is fed into the
606 * specified program as standard input.
607 */
608static FILE *logfp;
609static char *message;
610static List *changes;
611
612void
613Update_Logfile (repository, xmessage, xlogfp, xchanges)
614    char *repository;
615    char *xmessage;
616    FILE *xlogfp;
617    List *xchanges;
618{
619    /* nothing to do if the list is empty */
620    if (xchanges == NULL || xchanges->list->next == xchanges->list)
621	return;
622
623    /* set up static vars for update_logfile_proc */
624    message = xmessage;
625    logfp = xlogfp;
626    changes = xchanges;
627
628    /* call Parse_Info to do the actual logfile updates */
629    (void) Parse_Info (CVSROOTADM_LOGINFO, repository, update_logfile_proc, 1);
630}
631
632/*
633 * callback proc to actually do the logfile write from Update_Logfile
634 */
635static int
636update_logfile_proc (repository, filter)
637    char *repository;
638    char *filter;
639{
640    return (logfile_write (repository, filter, message, logfp, changes));
641}
642
643/*
644 * concatenate each filename/version onto str_list
645 */
646static int
647title_proc (p, closure)
648    Node *p;
649    void *closure;
650{
651    struct logfile_info *li;
652    char *c;
653
654    li = (struct logfile_info *) p->data;
655    if (li->type == type)
656    {
657	/* Until we decide on the correct logging solution when we add
658	   directories or perform imports, T_TITLE nodes will only
659	   tack on the name provided, regardless of the format string.
660	   You can verify that this assumption is safe by checking the
661	   code in add.c (add_directory) and import.c (import). */
662
663	str_list = xrealloc (str_list, strlen (str_list) + 5);
664	(void) strcat (str_list, " ");
665
666	if (li->type == T_TITLE)
667	{
668	    str_list = xrealloc (str_list,
669				 strlen (str_list) + strlen (p->key) + 5);
670	    (void) strcat (str_list, p->key);
671	}
672	else
673	{
674	    /* All other nodes use the format string. */
675
676	    for (c = str_list_format; *c != '\0'; c++)
677	    {
678		switch (*c)
679		{
680		case 's':
681		    str_list =
682			xrealloc (str_list,
683				  strlen (str_list) + strlen (p->key) + 5);
684		    (void) strcat (str_list, p->key);
685		    break;
686		case 'V':
687		    str_list =
688			xrealloc (str_list,
689				  (strlen (str_list)
690				   + (li->rev_old ? strlen (li->rev_old) : 0)
691				   + 10)
692				  );
693		    (void) strcat (str_list, (li->rev_old
694					      ? li->rev_old : "NONE"));
695		    break;
696		case 'v':
697		    str_list =
698			xrealloc (str_list,
699				  (strlen (str_list)
700				   + (li->rev_new ? strlen (li->rev_new) : 0)
701				   + 10)
702				  );
703		    (void) strcat (str_list, (li->rev_new
704					      ? li->rev_new : "NONE"));
705		    break;
706		/* All other characters, we insert an empty field (but
707		   we do put in the comma separating it from other
708		   fields).  This way if future CVS versions add formatting
709		   characters, one can write a loginfo file which at least
710		   won't blow up on an old CVS.  */
711		/* Note that people who have to deal with spaces in file
712		   and directory names are using space to get a known
713		   delimiter for the directory name, so it's probably
714		   not a good idea to ever define that as a formatting
715		   character.  */
716		}
717		if (*(c + 1) != '\0')
718		{
719		    str_list = xrealloc (str_list, strlen (str_list) + 5);
720		    (void) strcat (str_list, ",");
721		}
722	    }
723	}
724    }
725    return (0);
726}
727
728/*
729 * Writes some stuff to the logfile "filter" and returns the status of the
730 * filter program.
731 */
732static int
733logfile_write (repository, filter, message, logfp, changes)
734    char *repository;
735    char *filter;
736    char *message;
737    FILE *logfp;
738    List *changes;
739{
740    FILE *pipefp;
741    char *prog;
742    char *cp;
743    int c;
744    int pipestatus;
745    char *fmt_percent;		/* the location of the percent sign
746				   that starts the format string. */
747
748    /* The user may specify a format string as part of the filter.
749       Originally, `%s' was the only valid string.  The string that
750       was substituted for it was:
751
752         <repository-name> <file1> <file2> <file3> ...
753
754       Each file was either a new directory/import (T_TITLE), or a
755       added (T_ADDED), modified (T_MODIFIED), or removed (T_REMOVED)
756       file.
757
758       It is desirable to preserve that behavior so lots of commitlog
759       scripts won't die when they get this new code.  At the same
760       time, we'd like to pass other information about the files (like
761       version numbers, statuses, or checkin times).
762
763       The solution is to allow a format string that allows us to
764       specify those other pieces of information.  The format string
765       will be composed of `%' followed by a single format character,
766       or followed by a set of format characters surrounded by `{' and
767       `}' as separators.  The format characters are:
768
769         s = file name
770	 V = old version number (pre-checkin)
771	 v = new version number (post-checkin)
772
773       For example, valid format strings are:
774
775         %{}
776	 %s
777	 %{s}
778	 %{sVv}
779
780       There's no reason that more items couldn't be added (like
781       modification date or file status [added, modified, updated,
782       etc.]) -- the code modifications would be minimal (logmsg.c
783       (title_proc) and commit.c (check_fileproc)).
784
785       The output will be a string of tokens separated by spaces.  For
786       backwards compatibility, the the first token will be the
787       repository name.  The rest of the tokens will be
788       comma-delimited lists of the information requested in the
789       format string.  For example, if `/u/src/master' is the
790       repository, `%{sVv}' is the format string, and three files
791       (ChangeLog, Makefile, foo.c) were modified, the output might
792       be:
793
794         /u/src/master ChangeLog,1.1,1.2 Makefile,1.3,1.4 foo.c,1.12,1.13
795
796       Why this duplicates the old behavior when the format string is
797       `%s' is left as an exercise for the reader. */
798
799    fmt_percent = strchr (filter, '%');
800    if (fmt_percent)
801    {
802	int len;
803	char *srepos;
804	char *fmt_begin, *fmt_end;	/* beginning and end of the
805					   format string specified in
806					   filter. */
807	char *fmt_continue;		/* where the string continues
808					   after the format string (we
809					   might skip a '}') somewhere
810					   in there... */
811
812	/* Grab the format string. */
813
814	if ((*(fmt_percent + 1) == ' ') || (*(fmt_percent + 1) == '\0'))
815	{
816	    /* The percent stands alone.  This is an error.  We could
817	       be treating ' ' like any other formatting character, but
818	       using it as a formatting character seems like it would be
819	       a mistake.  */
820
821	    /* Would be nice to also be giving the line number.  */
822	    error (0, 0, "loginfo: '%%' not followed by formatting character");
823	    fmt_begin = fmt_percent + 1;
824	    fmt_end = fmt_begin;
825	    fmt_continue = fmt_begin;
826	}
827	else if (*(fmt_percent + 1) == '{')
828	{
829	    /* The percent has a set of characters following it. */
830
831	    fmt_begin = fmt_percent + 2;
832	    fmt_end = strchr (fmt_begin, '}');
833	    if (fmt_end)
834	    {
835		/* Skip over the '}' character. */
836
837		fmt_continue = fmt_end + 1;
838	    }
839	    else
840	    {
841		/* There was no close brace -- assume that format
842                   string continues to the end of the line. */
843
844		/* Would be nice to also be giving the line number.  */
845		error (0, 0, "loginfo: '}' missing");
846		fmt_end = fmt_begin + strlen (fmt_begin);
847		fmt_continue = fmt_end;
848	    }
849	}
850	else
851	{
852	    /* The percent has a single character following it.  FIXME:
853	       %% should expand to a regular percent sign.  */
854
855	    fmt_begin = fmt_percent + 1;
856	    fmt_end = fmt_begin + 1;
857	    fmt_continue = fmt_end;
858	}
859
860	len = fmt_end - fmt_begin;
861	str_list_format = xmalloc (len + 1);
862	strncpy (str_list_format, fmt_begin, len);
863	str_list_format[len] = '\0';
864
865	/* Allocate an initial chunk of memory.  As we build up the string
866	   we will realloc it.  */
867	if (!str_list)
868	    str_list = xmalloc (1);
869	str_list[0] = '\0';
870
871	/* Add entries to the string.  Don't bother looking for
872           entries if the format string is empty. */
873
874	if (str_list_format[0] != '\0')
875	{
876	    type = T_TITLE;
877	    (void) walklist (changes, title_proc, NULL);
878	    type = T_ADDED;
879	    (void) walklist (changes, title_proc, NULL);
880	    type = T_MODIFIED;
881	    (void) walklist (changes, title_proc, NULL);
882	    type = T_REMOVED;
883	    (void) walklist (changes, title_proc, NULL);
884	}
885
886	free (str_list_format);
887
888	/* Construct the final string. */
889
890	srepos = Short_Repository (repository);
891
892	prog = cp = xmalloc ((fmt_percent - filter) + 2 * strlen (srepos)
893			+ 2 * strlen (str_list) + strlen (fmt_continue)
894			+ 10);
895	(void) memcpy (cp, filter, fmt_percent - filter);
896	cp += fmt_percent - filter;
897	*cp++ = '"';
898	cp = shell_escape (cp, srepos);
899	cp = shell_escape (cp, str_list);
900	*cp++ = '"';
901	(void) strcpy (cp, fmt_continue);
902
903	/* To be nice, free up some memory. */
904
905	free (str_list);
906	str_list = (char *) NULL;
907    }
908    else
909    {
910	/* There's no format string. */
911	prog = xstrdup (filter);
912    }
913
914    if ((pipefp = run_popen (prog, "w")) == NULL)
915    {
916	if (!noexec)
917	    error (0, 0, "cannot write entry to log filter: %s", prog);
918	free (prog);
919	return (1);
920    }
921    (void) fprintf (pipefp, "Update of %s\n", repository);
922    (void) fprintf (pipefp, "In directory %s:", hostname);
923    cp = xgetwd ();
924    if (cp == NULL)
925	fprintf (pipefp, "<cannot get working directory: %s>\n\n",
926		 strerror (errno));
927    else
928    {
929	fprintf (pipefp, "%s\n\n", cp);
930	free (cp);
931    }
932
933    setup_tmpfile (pipefp, "", changes);
934    (void) fprintf (pipefp, "Log Message:\n%s\n", message);
935    if (logfp != (FILE *) 0)
936    {
937	(void) fprintf (pipefp, "Status:\n");
938	rewind (logfp);
939	while ((c = getc (logfp)) != EOF)
940	    (void) putc ((char) c, pipefp);
941    }
942    free (prog);
943    pipestatus = pclose (pipefp);
944    return ((pipestatus == -1) || (pipestatus == 127)) ? 1 : 0;
945}
946
947/*
948 * We choose to use the *last* match within the editinfo file for this
949 * repository.  This allows us to have a global editinfo program for the
950 * root of some hierarchy, for example, and different ones within different
951 * sub-directories of the root (like a special checker for changes made to
952 * the "src" directory versus changes made to the "doc" or "test"
953 * directories.
954 */
955/* ARGSUSED */
956static int
957editinfo_proc(repository, editor)
958    char *repository;
959    char *editor;
960{
961    /* nothing to do if the last match is the same as this one */
962    if (editinfo_editor && strcmp (editinfo_editor, editor) == 0)
963	return (0);
964    if (editinfo_editor)
965	free (editinfo_editor);
966
967    editinfo_editor = xstrdup (editor);
968    return (0);
969}
970
971/*  This routine is calld by Parse_Info.  it asigns the name of the
972 *  message verification script to the global variable verify_script
973 */
974static int
975verifymsg_proc (repository, script)
976    char *repository;
977    char *script;
978{
979    if (verifymsg_script && strcmp (verifymsg_script, script) == 0)
980	return (0);
981    if (verifymsg_script)
982	free (verifymsg_script);
983    verifymsg_script = xstrdup (script);
984    return (0);
985}
986