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