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