xref: /openbsd-src/gnu/usr.bin/cvs/src/logmsg.c (revision a4afd6dad3fba28f80e70208181c06c482259988)
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 1.4 kit.
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, char *title,
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 
22 static FILE *fp;
23 static char *str_list;
24 static char *editinfo_editor;
25 static Ctype type;
26 
27 /*
28  * Puts a standard header on the output which is either being prepared for an
29  * editor session, or being sent to a logfile program.  The modified, added,
30  * and removed files are included (if any) and formatted to look pretty. */
31 static char *prefix;
32 static int col;
33 static char *tag;
34 static void
35 setup_tmpfile (xfp, xprefix, changes)
36     FILE *xfp;
37     char *xprefix;
38     List *changes;
39 {
40     /* set up statics */
41     fp = xfp;
42     prefix = xprefix;
43 
44     type = T_MODIFIED;
45     if (walklist (changes, find_type, NULL) != 0)
46     {
47 	(void) fprintf (fp, "%sModified Files:\n", prefix);
48 	col = 0;
49 	(void) walklist (changes, fmt_proc, NULL);
50 	(void) fprintf (fp, "\n");
51 	if (tag != NULL)
52 	{
53 	    free (tag);
54 	    tag = NULL;
55 	}
56     }
57     type = T_ADDED;
58     if (walklist (changes, find_type, NULL) != 0)
59     {
60 	(void) fprintf (fp, "%sAdded Files:\n", prefix);
61 	col = 0;
62 	(void) walklist (changes, fmt_proc, NULL);
63 	(void) fprintf (fp, "\n");
64 	if (tag != NULL)
65 	{
66 	    free (tag);
67 	    tag = NULL;
68 	}
69     }
70     type = T_REMOVED;
71     if (walklist (changes, find_type, NULL) != 0)
72     {
73 	(void) fprintf (fp, "%sRemoved Files:\n", prefix);
74 	col = 0;
75 	(void) walklist (changes, fmt_proc, NULL);
76 	(void) fprintf (fp, "\n");
77 	if (tag != NULL)
78 	{
79 	    free (tag);
80 	    tag = NULL;
81 	}
82     }
83 }
84 
85 /*
86  * Looks for nodes of a specified type and returns 1 if found
87  */
88 static int
89 find_type (p, closure)
90     Node *p;
91     void *closure;
92 {
93     struct logfile_info *li;
94 
95     li = (struct logfile_info *) p->data;
96     if (li->type == type)
97 	return (1);
98     else
99 	return (0);
100 }
101 
102 /*
103  * Breaks the files list into reasonable sized lines to avoid line wrap...
104  * all in the name of pretty output.  It only works on nodes whose types
105  * match the one we're looking for
106  */
107 static int
108 fmt_proc (p, closure)
109     Node *p;
110     void *closure;
111 {
112     struct logfile_info *li;
113 
114     li = (struct logfile_info *) p->data;
115     if (li->type == type)
116     {
117         if (li->tag == NULL
118 	    ? tag != NULL
119 	    : tag == NULL || strcmp (tag, li->tag) != 0)
120 	{
121 	    if (col > 0)
122 	        (void) fprintf (fp, "\n");
123 	    (void) fprintf (fp, "%s", prefix);
124 	    col = strlen (prefix);
125 	    while (col < 6)
126 	    {
127 	        (void) fprintf (fp, " ");
128 		++col;
129 	    }
130 
131 	    if (li->tag == NULL)
132 	        (void) fprintf (fp, "No tag");
133 	    else
134 	        (void) fprintf (fp, "Tag: %s", li->tag);
135 
136 	    if (tag != NULL)
137 	        free (tag);
138 	    tag = xstrdup (li->tag);
139 
140 	    /* Force a new line.  */
141 	    col = 70;
142 	}
143 
144 	if (col == 0)
145 	{
146 	    (void) fprintf (fp, "%s\t", prefix);
147 	    col = 8;
148 	}
149 	else if (col > 8 && (col + (int) strlen (p->key)) > 70)
150 	{
151 	    (void) fprintf (fp, "\n%s\t", prefix);
152 	    col = 8;
153 	}
154 	(void) fprintf (fp, "%s ", p->key);
155 	col += strlen (p->key) + 1;
156     }
157     return (0);
158 }
159 
160 /*
161  * Builds a temporary file using setup_tmpfile() and invokes the user's
162  * editor on the file.  The header garbage in the resultant file is then
163  * stripped and the log message is stored in the "message" argument.
164  *
165  * If REPOSITORY is non-NULL, process rcsinfo for that repository; if it
166  * is NULL, use the CVSADM_TEMPLATE file instead.
167  */
168 void
169 do_editor (dir, messagep, repository, changes)
170     char *dir;
171     char **messagep;
172     char *repository;
173     List *changes;
174 {
175     static int reuse_log_message = 0;
176     char *line;
177     int line_length;
178     size_t line_chars_allocated;
179     char *fname;
180     struct stat pre_stbuf, post_stbuf;
181     int retcode = 0;
182     char *p;
183 
184     if (noexec || reuse_log_message)
185 	return;
186 
187     /* Abort creation of temp file if no editor is defined */
188     if (strcmp (Editor, "") == 0 && !editinfo_editor)
189 	error(1, 0, "no editor defined, must use -e or -m");
190 
191     /* Create a temporary file */
192     fname = cvs_temp_name ();
193   again:
194     if ((fp = CVS_FOPEN (fname, "w+")) == NULL)
195 	error (1, 0, "cannot create temporary file %s", fname);
196 
197     if (*messagep)
198     {
199 	(void) fprintf (fp, "%s", *messagep);
200 
201 	if ((*messagep)[strlen (*messagep) - 1] != '\n')
202 	    (void) fprintf (fp, "\n");
203     }
204     else
205 	(void) fprintf (fp, "\n");
206 
207     if (repository != NULL)
208 	/* tack templates on if necessary */
209 	(void) Parse_Info (CVSROOTADM_RCSINFO, repository, rcsinfo_proc, 1);
210     else
211     {
212 	FILE *tfp;
213 	char buf[1024];
214 	char *p;
215 	size_t n;
216 	size_t nwrite;
217 
218 	/* Why "b"?  */
219 	tfp = CVS_FOPEN (CVSADM_TEMPLATE, "rb");
220 	if (tfp == NULL)
221 	{
222 	    if (!existence_error (errno))
223 		error (1, errno, "cannot read %s", CVSADM_TEMPLATE);
224 	}
225 	else
226 	{
227 	    while (!feof (tfp))
228 	    {
229 		n = fread (buf, 1, sizeof buf, tfp);
230 		nwrite = n;
231 		p = buf;
232 		while (nwrite > 0)
233 		{
234 		    n = fwrite (p, 1, nwrite, fp);
235 		    nwrite -= n;
236 		    p += n;
237 		}
238 		if (ferror (tfp))
239 		    error (1, errno, "cannot read %s", CVSADM_TEMPLATE);
240 	    }
241 	    if (fclose (tfp) < 0)
242 		error (0, errno, "cannot close %s", CVSADM_TEMPLATE);
243 	}
244     }
245 
246     (void) fprintf (fp,
247   "%s----------------------------------------------------------------------\n",
248 		    CVSEDITPREFIX);
249     (void) fprintf (fp,
250   "%sEnter Log.  Lines beginning with `%s' are removed automatically\n%s\n",
251 		    CVSEDITPREFIX, CVSEDITPREFIX, CVSEDITPREFIX);
252     if (dir != NULL && *dir)
253 	(void) fprintf (fp, "%sCommitting in %s\n%s\n", CVSEDITPREFIX,
254 			dir, CVSEDITPREFIX);
255     if (changes != NULL)
256 	setup_tmpfile (fp, CVSEDITPREFIX, changes);
257     (void) fprintf (fp,
258   "%s----------------------------------------------------------------------\n",
259 		    CVSEDITPREFIX);
260 
261     /* finish off the temp file */
262     if (fclose (fp) == EOF)
263         error (1, errno, "%s", fname);
264     if ( CVS_STAT (fname, &pre_stbuf) == -1)
265 	pre_stbuf.st_mtime = 0;
266 
267     if (editinfo_editor)
268 	free (editinfo_editor);
269     editinfo_editor = (char *) NULL;
270     if (repository != NULL)
271 	(void) Parse_Info (CVSROOTADM_EDITINFO, repository, editinfo_proc, 0);
272 
273     /* run the editor */
274     run_setup ("%s", editinfo_editor ? editinfo_editor : Editor);
275     run_arg (fname);
276     if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY,
277 			     RUN_NORMAL | RUN_SIGIGNORE)) != 0)
278 	error (editinfo_editor ? 1 : 0, retcode == -1 ? errno : 0,
279 	       editinfo_editor ? "Logfile verification failed" :
280 	       "warning: editor session failed");
281 
282     /* put the entire message back into the *messagep variable */
283 
284     fp = open_file (fname, "r");
285 
286     if (*messagep)
287 	free (*messagep);
288 
289     if ( CVS_STAT (fname, &post_stbuf) != 0)
290 	    error (1, errno, "cannot find size of temp file %s", fname);
291 
292     if (post_stbuf.st_size == 0)
293 	*messagep = NULL;
294     else
295     {
296 	/* On NT, we might read less than st_size bytes, but we won't
297 	   read more.  So this works.  */
298 	*messagep = (char *) xmalloc (post_stbuf.st_size + 1);
299  	*messagep[0] = '\0';
300     }
301 
302     line = NULL;
303     line_chars_allocated = 0;
304 
305     if (*messagep)
306     {
307 	p = *messagep;
308 	while (1)
309 	{
310 	    line_length = getline (&line, &line_chars_allocated, fp);
311 	    if (line_length == -1)
312 	    {
313 		if (ferror (fp))
314 		    error (0, errno, "warning: cannot read %s", fname);
315 		break;
316 	    }
317 	    if (strncmp (line, CVSEDITPREFIX, sizeof (CVSEDITPREFIX) - 1) == 0)
318 		continue;
319 	    (void) strcpy (p, line);
320 	    p += line_length;
321 	}
322     }
323     if (fclose (fp) < 0)
324 	error (0, errno, "warning: cannot close %s", fname);
325 
326     if (pre_stbuf.st_mtime == post_stbuf.st_mtime ||
327 	*messagep == NULL ||
328 	strcmp (*messagep, "\n") == 0)
329     {
330 	for (;;)
331 	{
332 	    (void) printf ("\nLog message unchanged or not specified\n");
333 	    (void) printf ("a)bort, c)ontinue, e)dit, !)reuse this message unchanged for remaining dirs\n");
334 	    (void) printf ("Action: (continue) ");
335 	    (void) fflush (stdout);
336 	    line_length = getline (&line, &line_chars_allocated, stdin);
337 	    if (line_length <= 0
338 		    || *line == '\n' || *line == 'c' || *line == 'C')
339 		break;
340 	    if (*line == 'a' || *line == 'A')
341 		{
342 		    if (unlink_file (fname) < 0)
343 			error (0, errno, "warning: cannot remove temp file %s", fname);
344 		    error (1, 0, "aborted by user");
345 		}
346 	    if (*line == 'e' || *line == 'E')
347 		goto again;
348 	    if (*line == '!')
349 	    {
350 		reuse_log_message = 1;
351 		break;
352 	    }
353 	    (void) printf ("Unknown input\n");
354 	}
355     }
356     if (line)
357 	free (line);
358     if (unlink_file (fname) < 0)
359 	error (0, errno, "warning: cannot remove temp file %s", fname);
360     free (fname);
361 }
362 
363 /*
364  * callback proc for Parse_Info for rcsinfo templates this routine basically
365  * copies the matching template onto the end of the tempfile we are setting
366  * up
367  */
368 /* ARGSUSED */
369 static int
370 rcsinfo_proc (repository, template)
371     char *repository;
372     char *template;
373 {
374     static char *last_template;
375     FILE *tfp;
376 
377     /* nothing to do if the last one included is the same as this one */
378     if (last_template && strcmp (last_template, template) == 0)
379 	return (0);
380     if (last_template)
381 	free (last_template);
382     last_template = xstrdup (template);
383 
384     if ((tfp = CVS_FOPEN (template, "r")) != NULL)
385     {
386 	char *line = NULL;
387 	size_t line_chars_allocated = 0;
388 
389 	while (getline (&line, &line_chars_allocated, tfp) >= 0)
390 	    (void) fputs (line, fp);
391 	if (ferror (tfp))
392 	    error (0, errno, "warning: cannot read %s", template);
393 	if (fclose (tfp) < 0)
394 	    error (0, errno, "warning: cannot close %s", template);
395 	if (line)
396 	    free (line);
397 	return (0);
398     }
399     else
400     {
401 	error (0, errno, "Couldn't open rcsinfo template file %s", template);
402 	return (1);
403     }
404 }
405 
406 /*
407  * Uses setup_tmpfile() to pass the updated message on directly to any
408  * logfile programs that have a regular expression match for the checked in
409  * directory in the source repository.  The log information is fed into the
410  * specified program as standard input.
411  */
412 static char *title;
413 static FILE *logfp;
414 static char *message;
415 static List *changes;
416 
417 void
418 Update_Logfile (repository, xmessage, xlogfp, xchanges)
419     char *repository;
420     char *xmessage;
421     FILE *xlogfp;
422     List *xchanges;
423 {
424     char *srepos;
425 
426     /* nothing to do if the list is empty */
427     if (xchanges == NULL || xchanges->list->next == xchanges->list)
428 	return;
429 
430     /* set up static vars for update_logfile_proc */
431     message = xmessage;
432     logfp = xlogfp;
433     changes = xchanges;
434 
435     /* figure out a good title string */
436     srepos = Short_Repository (repository);
437 
438     /* allocate a chunk of memory to hold the title string */
439     if (!str_list)
440 	str_list = xmalloc (MAXLISTLEN);
441     str_list[0] = '\0';
442 
443     type = T_TITLE;
444     (void) walklist (changes, title_proc, NULL);
445     type = T_ADDED;
446     (void) walklist (changes, title_proc, NULL);
447     type = T_MODIFIED;
448     (void) walklist (changes, title_proc, NULL);
449     type = T_REMOVED;
450     (void) walklist (changes, title_proc, NULL);
451     title = xmalloc (strlen (srepos) + strlen (str_list) + 1 + 2); /* for 's */
452     (void) sprintf (title, "'%s%s'", srepos, str_list);
453 
454     /* to be nice, free up this chunk of memory */
455     free (str_list);
456     str_list = (char *) NULL;
457 
458     /* call Parse_Info to do the actual logfile updates */
459     (void) Parse_Info (CVSROOTADM_LOGINFO, repository, update_logfile_proc, 1);
460 
461     /* clean up */
462     free (title);
463 }
464 
465 /*
466  * callback proc to actually do the logfile write from Update_Logfile
467  */
468 static int
469 update_logfile_proc (repository, filter)
470     char *repository;
471     char *filter;
472 {
473     return (logfile_write (repository, filter, title, message, logfp,
474 			   changes));
475 }
476 
477 /*
478  * concatenate each name onto str_list
479  */
480 static int
481 title_proc (p, closure)
482     Node *p;
483     void *closure;
484 {
485     struct logfile_info *li;
486 
487     li = (struct logfile_info *) p->data;
488     if (li->type == type)
489     {
490 	(void) strcat (str_list, " ");
491 	(void) strcat (str_list, p->key);
492     }
493     return (0);
494 }
495 
496 /*
497  * Since some systems don't define this...
498  */
499 #ifndef MAXHOSTNAMELEN
500 #define	MAXHOSTNAMELEN	256
501 #endif
502 
503 /*
504  * Writes some stuff to the logfile "filter" and returns the status of the
505  * filter program.
506  */
507 static int
508 logfile_write (repository, filter, title, message, logfp, changes)
509     char *repository;
510     char *filter;
511     char *title;
512     char *message;
513     FILE *logfp;
514     List *changes;
515 {
516     char cwd[PATH_MAX];
517     FILE *pipefp;
518     char *prog = xmalloc (MAXPROGLEN);
519     char *cp;
520     int c;
521     int pipestatus;
522 
523     /*
524      * Only 1 %s argument is supported in the filter
525      */
526     (void) sprintf (prog, filter, title);
527     if ((pipefp = run_popen (prog, "w")) == NULL)
528     {
529 	if (!noexec)
530 	    error (0, 0, "cannot write entry to log filter: %s", prog);
531 	free (prog);
532 	return (1);
533     }
534     (void) fprintf (pipefp, "Update of %s\n", repository);
535     (void) fprintf (pipefp, "In directory %s:%s\n\n", hostname,
536 		    ((cp = getwd (cwd)) != NULL) ? cp : cwd);
537     setup_tmpfile (pipefp, "", changes);
538     (void) fprintf (pipefp, "Log Message:\n%s\n", message);
539     if (logfp != (FILE *) 0)
540     {
541 	(void) fprintf (pipefp, "Status:\n");
542 	rewind (logfp);
543 	while ((c = getc (logfp)) != EOF)
544 	    (void) putc ((char) c, pipefp);
545     }
546     free (prog);
547     pipestatus = pclose (pipefp);
548     return ((pipestatus == -1) || (pipestatus == 127)) ? 1 : 0;
549 }
550 
551 /*
552  * We choose to use the *last* match within the editinfo file for this
553  * repository.  This allows us to have a global editinfo program for the
554  * root of some hierarchy, for example, and different ones within different
555  * sub-directories of the root (like a special checker for changes made to
556  * the "src" directory versus changes made to the "doc" or "test"
557  * directories.
558  */
559 /* ARGSUSED */
560 static int
561 editinfo_proc(repository, editor)
562     char *repository;
563     char *editor;
564 {
565     /* nothing to do if the last match is the same as this one */
566     if (editinfo_editor && strcmp (editinfo_editor, editor) == 0)
567 	return (0);
568     if (editinfo_editor)
569 	free (editinfo_editor);
570 
571     editinfo_editor = xstrdup (editor);
572     return (0);
573 }
574