xref: /dragonfly/contrib/cvs-1.12/src/edit.c (revision 36a3d1d6)
1 /* Implementation for "cvs edit", "cvs watch on", and related commands
2 
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published by
5    the Free Software Foundation; either version 2, or (at your option)
6    any later version.
7 
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY; without even the implied warranty of
10    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11    GNU General Public License for more details.  */
12 
13 #include "cvs.h"
14 #include "getline.h"
15 #include "yesno.h"
16 #include "watch.h"
17 #include "edit.h"
18 #include "fileattr.h"
19 
20 static int watch_onoff (int, char **);
21 
22 static bool check_edited = false;
23 static int setting_default;
24 static int turning_on;
25 
26 static bool setting_tedit;
27 static bool setting_tunedit;
28 static bool setting_tcommit;
29 
30 
31 
32 static int
33 onoff_fileproc (void *callerdat, struct file_info *finfo)
34 {
35     fileattr_get0 (finfo->file, "_watched");
36     fileattr_set (finfo->file, "_watched", turning_on ? "" : NULL);
37     return 0;
38 }
39 
40 
41 
42 static int
43 onoff_filesdoneproc (void *callerdat, int err, const char *repository,
44                      const char *update_dir, List *entries)
45 {
46     if (setting_default)
47     {
48 	fileattr_get0 (NULL, "_watched");
49 	fileattr_set (NULL, "_watched", turning_on ? "" : NULL);
50     }
51     return err;
52 }
53 
54 
55 
56 static int
57 watch_onoff (int argc, char **argv)
58 {
59     int c;
60     int local = 0;
61     int err;
62 
63     optind = 0;
64     while ((c = getopt (argc, argv, "+lR")) != -1)
65     {
66 	switch (c)
67 	{
68 	    case 'l':
69 		local = 1;
70 		break;
71 	    case 'R':
72 		local = 0;
73 		break;
74 	    case '?':
75 	    default:
76 		usage (watch_usage);
77 		break;
78 	}
79     }
80     argc -= optind;
81     argv += optind;
82 
83 #ifdef CLIENT_SUPPORT
84     if (current_parsed_root->isremote)
85     {
86 	start_server ();
87 
88 	ign_setup ();
89 
90 	if (local)
91 	    send_arg ("-l");
92 	send_arg ("--");
93 	send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
94 	send_file_names (argc, argv, SEND_EXPAND_WILD);
95 	send_to_server (turning_on ? "watch-on\012" : "watch-off\012", 0);
96 	return get_responses_and_close ();
97     }
98 #endif /* CLIENT_SUPPORT */
99 
100     setting_default = (argc <= 0);
101 
102     lock_tree_promotably (argc, argv, local, W_LOCAL, 0);
103 
104     err = start_recursion (onoff_fileproc, onoff_filesdoneproc, NULL, NULL,
105 			   NULL, argc, argv, local, W_LOCAL, 0, CVS_LOCK_WRITE,
106 			   NULL, 0, NULL);
107 
108     Lock_Cleanup ();
109     return err;
110 }
111 
112 int
113 watch_on (int argc, char **argv)
114 {
115     turning_on = 1;
116     return watch_onoff (argc, argv);
117 }
118 
119 int
120 watch_off (int argc, char **argv)
121 {
122     turning_on = 0;
123     return watch_onoff (argc, argv);
124 }
125 
126 
127 
128 static int
129 dummy_fileproc (void *callerdat, struct file_info *finfo)
130 {
131     /* This is a pretty hideous hack, but the gist of it is that recurse.c
132        won't call notify_check unless there is a fileproc, so we can't just
133        pass NULL for fileproc.  */
134     return 0;
135 }
136 
137 
138 
139 /* Check for and process notifications.  Local only.  I think that doing
140    this as a fileproc is the only way to catch all the
141    cases (e.g. foo/bar.c), even though that means checking over and over
142    for the same CVSADM_NOTIFY file which we removed the first time we
143    processed the directory.  */
144 static int
145 ncheck_fileproc (void *callerdat, struct file_info *finfo)
146 {
147     int notif_type;
148     char *filename;
149     char *val;
150     char *cp;
151     char *watches;
152 
153     FILE *fp;
154     char *line = NULL;
155     size_t line_len = 0;
156 
157     /* We send notifications even if noexec.  I'm not sure which behavior
158        is most sensible.  */
159 
160     fp = CVS_FOPEN (CVSADM_NOTIFY, "r");
161     if (fp == NULL)
162     {
163 	if (!existence_error (errno))
164 	    error (0, errno, "cannot open %s", CVSADM_NOTIFY);
165 	return 0;
166     }
167 
168     while (getline (&line, &line_len, fp) > 0)
169     {
170 	notif_type = line[0];
171 	if (notif_type == '\0')
172 	    continue;
173 	filename = line + 1;
174 	cp = strchr (filename, '\t');
175 	if (cp == NULL)
176 	    continue;
177 	*cp++ = '\0';
178 	val = cp;
179 	cp = strchr (val, '\t');
180 	if (cp == NULL)
181 	    continue;
182 	*cp++ = '+';
183 	cp = strchr (cp, '\t');
184 	if (cp == NULL)
185 	    continue;
186 	*cp++ = '+';
187 	cp = strchr (cp, '\t');
188 	if (cp == NULL)
189 	    continue;
190 	*cp++ = '\0';
191 	watches = cp;
192 	cp = strchr (cp, '\n');
193 	if (cp == NULL)
194 	    continue;
195 	*cp = '\0';
196 
197 	notify_do (notif_type, filename, finfo->update_dir, getcaller (), val,
198 		   watches, finfo->repository);
199     }
200     free (line);
201 
202     if (ferror (fp))
203 	error (0, errno, "cannot read %s", CVSADM_NOTIFY);
204     if (fclose (fp) < 0)
205 	error (0, errno, "cannot close %s", CVSADM_NOTIFY);
206 
207     if ( CVS_UNLINK (CVSADM_NOTIFY) < 0)
208 	error (0, errno, "cannot remove %s", CVSADM_NOTIFY);
209 
210     return 0;
211 }
212 
213 
214 
215 /* Look through the CVSADM_NOTIFY file and process each item there
216    accordingly.  */
217 static int
218 send_notifications (int argc, char **argv, int local)
219 {
220     int err = 0;
221 
222 #ifdef CLIENT_SUPPORT
223     /* OK, we've done everything which needs to happen on the client side.
224        Now we can try to contact the server; if we fail, then the
225        notifications stay in CVSADM_NOTIFY to be sent next time.  */
226     if (current_parsed_root->isremote)
227     {
228 	if (strcmp (cvs_cmd_name, "release") != 0)
229 	{
230 	    start_server ();
231 	    ign_setup ();
232 	}
233 
234 	err += start_recursion (dummy_fileproc, NULL, NULL, NULL, NULL, argc,
235 				argv, local, W_LOCAL, 0, 0, NULL, 0, NULL);
236 
237 	send_to_server ("noop\012", 0);
238 	if (strcmp (cvs_cmd_name, "release") == 0)
239 	    err += get_server_responses ();
240 	else
241 	    err += get_responses_and_close ();
242     }
243     else
244 #endif
245     {
246 	/* Local.  */
247 
248 	err += start_recursion (ncheck_fileproc, NULL, NULL, NULL, NULL, argc,
249 				argv, local, W_LOCAL, 0, CVS_LOCK_WRITE, NULL,
250 				0, NULL);
251 	Lock_Cleanup ();
252     }
253     return err;
254 }
255 
256 
257 
258 void editors_output (const char *fullname, const char *p)
259 {
260     cvs_output (fullname, 0);
261 
262     while (1)
263     {
264         cvs_output ("\t", 1);
265         while (*p != '>' && *p != '\0')
266             cvs_output (p++, 1);
267         if (*p == '\0')
268         {
269             /* Only happens if attribute is misformed.  */
270             cvs_output ("\n", 1);
271             break;
272         }
273         ++p;
274         cvs_output ("\t", 1);
275         while (1)
276         {
277             while (*p != '+' && *p != ',' && *p != '\0')
278                 cvs_output (p++, 1);
279             if (*p == '\0')
280             {
281                 cvs_output ("\n", 1);
282                 return;
283             }
284             if (*p == ',')
285             {
286                 ++p;
287                 break;
288             }
289             ++p;
290             cvs_output ("\t", 1);
291         }
292         cvs_output ("\n", 1);
293     }
294 }
295 
296 
297 
298 static int find_editors_and_output (struct file_info *finfo)
299 {
300     char *them;
301 
302     them = fileattr_get0 (finfo->file, "_editors");
303     if (them == NULL)
304         return 0;
305 
306     editors_output (finfo->fullname, them);
307 
308     return 0;
309 }
310 
311 
312 
313 /* Handle the client-side details of editing a file.
314  *
315  * These args could be const but this needs to fit the call_in_directory API.
316  */
317 void
318 edit_file (void *data, List *ent_list, const char *short_pathname,
319 	   const char *filename)
320 {
321     Node *node;
322     struct file_info finfo;
323     char *basefilename;
324 
325     xchmod (filename, 1);
326 
327     mkdir_if_needed (CVSADM_BASE);
328     basefilename = Xasprintf ("%s/%s", CVSADM_BASE, filename);
329     copy_file (filename, basefilename);
330     free (basefilename);
331 
332     node = findnode_fn (ent_list, filename);
333     if (node != NULL)
334     {
335 	finfo.file = filename;
336 	finfo.fullname = short_pathname;
337 	finfo.update_dir = dir_name (short_pathname);
338 	base_register (&finfo, ((Entnode *) node->data)->version);
339 	free ((char *)finfo.update_dir);
340     }
341 }
342 
343 
344 
345 static int
346 edit_fileproc (void *callerdat, struct file_info *finfo)
347 {
348     FILE *fp;
349     time_t now;
350     char *ascnow;
351     Vers_TS *vers;
352 
353 #if defined (CLIENT_SUPPORT)
354     assert (!(current_parsed_root->isremote && check_edited));
355 #else /* !CLIENT_SUPPORT */
356     assert (!check_edited);
357 #endif /* CLIENT_SUPPORT */
358 
359     if (noexec)
360 	return 0;
361 
362     vers = Version_TS (finfo, NULL, NULL, NULL, 1, 0);
363 
364     if (!vers->vn_user)
365     {
366 	error (0, 0, "no such file %s; ignored", finfo->fullname);
367 	return 1;
368     }
369 
370 #ifdef CLIENT_SUPPORT
371     if (!current_parsed_root->isremote)
372 #endif /* CLIENT_SUPPORT */
373     {
374         char *editors = fileattr_get0 (finfo->file, "_editors");
375         if (editors)
376         {
377 	    if (check_edited)
378 	    {
379 		/* In the !CHECK_EDIT case, this message is printed by
380 		 * server_notify.
381 		 */
382 		if (!quiet)
383 		    editors_output (finfo->fullname, editors);
384 		 /* Now warn the user if we skip the file, then return.  */
385 		if (!really_quiet)
386 		    error (0, 0, "Skipping file `%s' due to existing editors.",
387 			   finfo->fullname);
388 		return 1;
389 	    }
390             free (editors);
391         }
392     }
393 
394     fp = xfopen (CVSADM_NOTIFY, "a");
395 
396     (void) time (&now);
397     ascnow = asctime (gmtime (&now));
398     ascnow[24] = '\0';
399     /* Fix non-standard format.  */
400     if (ascnow[8] == '0') ascnow[8] = ' ';
401     fprintf (fp, "E%s\t%s -0000\t%s\t%s\t", finfo->file,
402 	     ascnow, hostname, CurDir);
403     if (setting_tedit)
404 	fprintf (fp, "E");
405     if (setting_tunedit)
406 	fprintf (fp, "U");
407     if (setting_tcommit)
408 	fprintf (fp, "C");
409     fprintf (fp, "\n");
410 
411     if (fclose (fp) < 0)
412     {
413 	if (finfo->update_dir[0] == '\0')
414 	    error (0, errno, "cannot close %s", CVSADM_NOTIFY);
415 	else
416 	    error (0, errno, "cannot close %s/%s", finfo->update_dir,
417 		   CVSADM_NOTIFY);
418     }
419 
420     /* Now stash the file away in CVSADM so that unedit can revert even if
421        it can't communicate with the server.  We stash away a writable
422        copy so that if the user removes the working file, then restores it
423        with "cvs update" (which clears _editors but does not update
424        CVSADM_BASE), then a future "cvs edit" can still win.  */
425     /* Could save a system call by only calling mkdir_if_needed if
426        trying to create the output file fails.  But copy_file isn't
427        set up to facilitate that.  */
428 #ifdef SERVER_SUPPORT
429     if (server_active)
430 	server_edit_file (finfo);
431     else
432 #endif /* SERVER_SUPPORT */
433 	edit_file (NULL, finfo->entries, finfo->fullname, finfo->file);
434 
435     return 0;
436 }
437 
438 static const char *const edit_usage[] =
439 {
440     "Usage: %s %s [-lRcf] [-a <action>]... [<file>]...\n",
441     "-l\tLocal directory only, not recursive.\n",
442     "-R\tProcess directories recursively (default).\n",
443     "-a\tSpecify action to register for temporary watch, one of:\n",
444     "  \t`edit', `unedit', `commit', `all', `none' (defaults to `all').\n",
445     "-c\tCheck for <file>s edited by others and abort if found.\n",
446     "-f\tAllow edit if <file>s are edited by others (default).\n",
447     "(Specify the --help global option for a list of other help options.)\n",
448     NULL
449 };
450 
451 int
452 edit (int argc, char **argv)
453 {
454     int local = 0;
455     int c;
456     int err = 0;
457     bool a_omitted, a_all, a_none;
458 
459     if (argc == -1)
460 	usage (edit_usage);
461 
462     a_omitted = true;
463     a_all = false;
464     a_none = false;
465     setting_tedit = false;
466     setting_tunedit = false;
467     setting_tcommit = false;
468     optind = 0;
469     while ((c = getopt (argc, argv, "+cflRa:")) != -1)
470     {
471 	switch (c)
472 	{
473             case 'c':
474                 check_edited = true;
475                 break;
476             case 'f':
477                 check_edited = false;
478                 break;
479 	    case 'l':
480 		local = 1;
481 		break;
482 	    case 'R':
483 		local = 0;
484 		break;
485 	    case 'a':
486 		a_omitted = false;
487 		if (strcmp (optarg, "edit") == 0)
488 		    setting_tedit = true;
489 		else if (strcmp (optarg, "unedit") == 0)
490 		    setting_tunedit = true;
491 		else if (strcmp (optarg, "commit") == 0)
492 		    setting_tcommit = true;
493 		else if (strcmp (optarg, "all") == 0)
494 		{
495 		    a_all = true;
496 		    a_none = false;
497 		    setting_tedit = true;
498 		    setting_tunedit = true;
499 		    setting_tcommit = true;
500 		}
501 		else if (strcmp (optarg, "none") == 0)
502 		{
503 		    a_none = true;
504 		    a_all = false;
505 		    setting_tedit = false;
506 		    setting_tunedit = false;
507 		    setting_tcommit = false;
508 		}
509 		else
510 		    usage (edit_usage);
511 		break;
512 	    case '?':
513 	    default:
514 		usage (edit_usage);
515 		break;
516 	}
517     }
518     argc -= optind;
519     argv += optind;
520 
521     if (strpbrk (hostname, "+,>;=\t\n") != NULL)
522 	error (1, 0,
523 	       "host name (%s) contains an invalid character (+,>;=\\t\\n)",
524 	       hostname);
525     if (strpbrk (CurDir, "+,>;=\t\n") != NULL)
526 	error (1, 0,
527 "current directory (%s) contains an invalid character (+,>;=\\t\\n)",
528 	       CurDir);
529 
530 #ifdef CLIENT_SUPPORT
531     if (check_edited && current_parsed_root->isremote)
532     {
533 	/* When CHECK_EDITED, we might as well contact the server and let it do
534 	 * the work since we don't want an edit unless we know it is safe.
535 	 *
536 	 * When !CHECK_EDITED, we set up notifications and then attempt to
537 	 * contact the server in order to allow disconnected edits.
538 	 */
539 	start_server();
540 
541 	if (!supported_request ("edit"))
542 	    error (1, 0, "Server does not support enforced advisory locks.");
543 
544 	ign_setup();
545 
546 	send_to_server ("Hostname ", 0);
547 	send_to_server (hostname, 0);
548 	send_to_server ("\012", 1);
549 	send_to_server ("LocalDir ", 0);
550 	send_to_server (CurDir, 0);
551 	send_to_server ("\012", 1);
552 
553 	if (local)
554 	    send_arg ("-l");
555 	send_arg ("-c");
556 	if (!a_omitted)
557 	{
558 	    if (a_all)
559 		option_with_arg ("-a", "all");
560 	    else if (a_none)
561 		option_with_arg ("-a", "none");
562 	    else
563 	    {
564 		if (setting_tedit)
565 		    option_with_arg ("-a", "edit");
566 		if (setting_tunedit)
567 		    option_with_arg ("-a", "unedit");
568 		if (setting_tcommit)
569 		    option_with_arg ("-a", "commit");
570 	    }
571 	}
572 	send_arg ("--");
573 	send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
574 	send_file_names (argc, argv, SEND_EXPAND_WILD);
575 	send_to_server ("edit\012", 0);
576 	return get_responses_and_close ();
577     }
578 #endif /* CLIENT_SUPPORT */
579 
580     /* Now, either SERVER_ACTIVE, local mode, or !CHECK_EDITED.  */
581 
582     if (a_omitted)
583     {
584 	setting_tedit = true;
585 	setting_tunedit = true;
586 	setting_tcommit = true;
587     }
588 
589     TRACE (TRACE_DATA, "edit(): EUC: %d%d%d edit-check: %d",
590 	   setting_tedit, setting_tunedit, setting_tcommit, check_edited);
591 
592     err = start_recursion (edit_fileproc, NULL, NULL, NULL, NULL, argc, argv,
593 			   local, W_LOCAL, 0, 0, NULL, 0, NULL);
594 
595     err += send_notifications (argc, argv, local);
596 
597     return err;
598 }
599 
600 static int unedit_fileproc (void *callerdat, struct file_info *finfo);
601 
602 static int
603 unedit_fileproc (void *callerdat, struct file_info *finfo)
604 {
605     FILE *fp;
606     time_t now;
607     char *ascnow;
608     char *basefilename = NULL;
609 
610     if (noexec)
611 	return 0;
612 
613     basefilename = Xasprintf ("%s/%s", CVSADM_BASE, finfo->file);
614     if (!isfile (basefilename))
615     {
616 	/* This file apparently was never cvs edit'd (e.g. we are uneditting
617 	   a directory where only some of the files were cvs edit'd.  */
618 	free (basefilename);
619 	return 0;
620     }
621 
622     if (xcmp (finfo->file, basefilename) != 0)
623     {
624 	printf ("%s has been modified; revert changes? ", finfo->fullname);
625 	fflush (stderr);
626 	fflush (stdout);
627 	if (!yesno ())
628 	{
629 	    /* "no".  */
630 	    free (basefilename);
631 	    return 0;
632 	}
633     }
634     rename_file (basefilename, finfo->file);
635     free (basefilename);
636 
637     fp = xfopen (CVSADM_NOTIFY, "a");
638 
639     (void) time (&now);
640     ascnow = asctime (gmtime (&now));
641     ascnow[24] = '\0';
642     /* Fix non-standard format.  */
643     if (ascnow[8] == '0') ascnow[8] = ' ';
644     fprintf (fp, "U%s\t%s -0000\t%s\t%s\t\n", finfo->file,
645 	     ascnow, hostname, CurDir);
646 
647     if (fclose (fp) < 0)
648     {
649 	if (finfo->update_dir[0] == '\0')
650 	    error (0, errno, "cannot close %s", CVSADM_NOTIFY);
651 	else
652 	    error (0, errno, "cannot close %s/%s", finfo->update_dir,
653 		   CVSADM_NOTIFY);
654     }
655 
656     /* Now update the revision number in CVS/Entries from CVS/Baserev.
657        The basic idea here is that we are reverting to the revision
658        that the user edited.  If we wanted "cvs update" to update
659        CVS/Base as we go along (so that an unedit could revert to the
660        current repository revision), we would need:
661 
662        update (or all send_files?) (client) needs to send revision in
663        new Entry-base request.  update (server/local) needs to check
664        revision against repository and send new Update-base response
665        (like Update-existing in that the file already exists.  While
666        we are at it, might try to clean up the syntax by having the
667        mode only in a "Mode" response, not in the Update-base itself).  */
668     {
669 	char *baserev;
670 	Node *node;
671 	Entnode *entdata;
672 
673 	baserev = base_get (finfo);
674 	node = findnode_fn (finfo->entries, finfo->file);
675 	/* The case where node is NULL probably should be an error or
676 	   something, but I don't want to think about it too hard right
677 	   now.  */
678 	if (node != NULL)
679 	{
680 	    entdata = node->data;
681 	    if (baserev == NULL)
682 	    {
683 		/* This can only happen if the CVS/Baserev file got
684 		   corrupted.  We suspect it might be possible if the
685 		   user interrupts CVS, although I haven't verified
686 		   that.  */
687 		error (0, 0, "%s not mentioned in %s", finfo->fullname,
688 		       CVSADM_BASEREV);
689 
690 		/* Since we don't know what revision the file derives from,
691 		   keeping it around would be asking for trouble.  */
692 		if (unlink_file (finfo->file) < 0)
693 		    error (0, errno, "cannot remove %s", finfo->fullname);
694 
695 		/* This is cheesy, in a sense; why shouldn't we do the
696 		   update for the user?  However, doing that would require
697 		   contacting the server, so maybe this is OK.  */
698 		error (0, 0, "run update to complete the unedit");
699 		return 0;
700 	    }
701 	    Register (finfo->entries, finfo->file, baserev, entdata->timestamp,
702 		      entdata->options, entdata->tag, entdata->date,
703 		      entdata->conflict);
704 	}
705 	free (baserev);
706 	base_deregister (finfo);
707     }
708 
709     xchmod (finfo->file, 0);
710     return 0;
711 }
712 
713 static const char *const unedit_usage[] =
714 {
715     "Usage: %s %s [-lR] [<file>]...\n",
716     "-l\tLocal directory only, not recursive.\n",
717     "-R\tProcess directories recursively (default).\n",
718     "(Specify the --help global option for a list of other help options.)\n",
719     NULL
720 };
721 
722 int
723 unedit (int argc, char **argv)
724 {
725     int local = 0;
726     int c;
727     int err;
728 
729     if (argc == -1)
730 	usage (unedit_usage);
731 
732     optind = 0;
733     while ((c = getopt (argc, argv, "+lR")) != -1)
734     {
735 	switch (c)
736 	{
737 	    case 'l':
738 		local = 1;
739 		break;
740 	    case 'R':
741 		local = 0;
742 		break;
743 	    case '?':
744 	    default:
745 		usage (unedit_usage);
746 		break;
747 	}
748     }
749     argc -= optind;
750     argv += optind;
751 
752     /* No need to readlock since we aren't doing anything to the
753        repository.  */
754     err = start_recursion (unedit_fileproc, NULL, NULL, NULL, NULL, argc, argv,
755 			   local, W_LOCAL, 0, 0, NULL, 0, NULL);
756 
757     err += send_notifications (argc, argv, local);
758 
759     return err;
760 }
761 
762 
763 
764 void
765 mark_up_to_date (const char *file)
766 {
767     char *base;
768 
769     /* The file is up to date, so we better get rid of an out of
770        date file in CVSADM_BASE.  */
771     base = Xasprintf ("%s/%s", CVSADM_BASE, file);
772     if (unlink_file (base) < 0 && ! existence_error (errno))
773 	error (0, errno, "cannot remove %s", file);
774     free (base);
775 }
776 
777 
778 
779 void
780 editor_set (const char *filename, const char *editor, const char *val)
781 {
782     char *edlist;
783     char *newlist;
784 
785     edlist = fileattr_get0 (filename, "_editors");
786     newlist = fileattr_modify (edlist, editor, val, '>', ',');
787     /* If the attributes is unchanged, don't rewrite the attribute file.  */
788     if (!((edlist == NULL && newlist == NULL)
789 	  || (edlist != NULL
790 	      && newlist != NULL
791 	      && strcmp (edlist, newlist) == 0)))
792 	fileattr_set (filename, "_editors", newlist);
793     if (edlist != NULL)
794 	free (edlist);
795     if (newlist != NULL)
796 	free (newlist);
797 }
798 
799 struct notify_proc_args {
800     /* What kind of notification, "edit", "tedit", etc.  */
801     const char *type;
802     /* User who is running the command which causes notification.  */
803     const char *who;
804     /* User to be notified.  */
805     const char *notifyee;
806     /* File.  */
807     const char *file;
808 };
809 
810 
811 
812 static int
813 notify_proc (const char *repository, const char *filter, void *closure)
814 {
815     char *cmdline;
816     FILE *pipefp;
817     const char *srepos = Short_Repository (repository);
818     struct notify_proc_args *args = closure;
819 
820     /*
821      * Cast any NULL arguments as appropriate pointers as this is an
822      * stdarg function and we need to be certain the caller gets what
823      * is expected.
824      */
825     cmdline = format_cmdline (
826 #ifdef SUPPORT_OLD_INFO_FMT_STRINGS
827 			      false, srepos,
828 #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
829 			      filter,
830 			      "c", "s", cvs_cmd_name,
831 #ifdef SERVER_SUPPORT
832 			      "R", "s", referrer ? referrer->original : "NONE",
833 #endif /* SERVER_SUPPORT */
834 			      "p", "s", srepos,
835 			      "r", "s", current_parsed_root->directory,
836 			      "s", "s", args->notifyee,
837 			      (char *) NULL);
838     if (!cmdline || !strlen (cmdline))
839     {
840 	if (cmdline) free (cmdline);
841 	error (0, 0, "pretag proc resolved to the empty string!");
842 	return 1;
843     }
844 
845     pipefp = run_popen (cmdline, "w");
846     if (pipefp == NULL)
847     {
848 	error (0, errno, "cannot write entry to notify filter: %s", cmdline);
849 	free (cmdline);
850 	return 1;
851     }
852 
853     fprintf (pipefp, "%s %s\n---\n", srepos, args->file);
854     fprintf (pipefp, "Triggered %s watch on %s\n", args->type, repository);
855     fprintf (pipefp, "By %s\n", args->who);
856 
857     /* Lots more potentially useful information we could add here; see
858        logfile_write for inspiration.  */
859 
860     free (cmdline);
861     return pclose (pipefp);
862 }
863 
864 
865 
866 /* FIXME: this function should have a way to report whether there was
867    an error so that server.c can know whether to report Notified back
868    to the client.  */
869 void
870 notify_do (int type, const char *filename, const char *update_dir,
871 	   const char *who, const char *val, const char *watches,
872 	   const char *repository)
873 {
874     static struct addremove_args blank;
875     struct addremove_args args;
876     char *watchers;
877     char *p;
878     char *endp;
879     char *nextp;
880 
881     /* Print out information on current editors if we were called during an
882      * edit.
883      */
884     if (type == 'E' && !check_edited && !quiet)
885     {
886 	char *editors = fileattr_get0 (filename, "_editors");
887 	if (editors)
888 	{
889 	    /* In the CHECK_EDIT case, this message is printed by
890 	     * edit_check.  It needs to be done here too since files
891 	     * which are found to be edited when CHECK_EDIT are not
892 	     * added to the notify list.
893 	     */
894 	    const char *tmp;
895 	    if (update_dir && *update_dir)
896 		tmp  = Xasprintf ("%s/%s", update_dir, filename);
897 	    else
898 		tmp = filename;
899 
900 	    editors_output (tmp, editors);
901 
902 	    if (update_dir && *update_dir) free ((char *)tmp);
903 	    free (editors);
904 	}
905     }
906 
907     /* Initialize fields to 0, NULL, or 0.0.  */
908     args = blank;
909     switch (type)
910     {
911 	case 'E':
912 	    if (strpbrk (val, ",>;=\n") != NULL)
913 	    {
914 		error (0, 0, "invalid character in editor value");
915 		return;
916 	    }
917 	    editor_set (filename, who, val);
918 	    break;
919 	case 'U':
920 	case 'C':
921 	    editor_set (filename, who, NULL);
922 	    break;
923 	default:
924 	    return;
925     }
926 
927     watchers = fileattr_get0 (filename, "_watchers");
928     p = watchers;
929     while (p != NULL)
930     {
931 	char *q;
932 	char *endq;
933 	char *nextq;
934 	char *notif;
935 
936 	endp = strchr (p, '>');
937 	if (endp == NULL)
938 	    break;
939 	nextp = strchr (p, ',');
940 
941 	if ((size_t)(endp - p) == strlen (who) && strncmp (who, p, endp - p) == 0)
942 	{
943 	    /* Don't notify user of their own changes.  Would perhaps
944 	       be better to check whether it is the same working
945 	       directory, not the same user, but that is hairy.  */
946 	    p = nextp == NULL ? nextp : nextp + 1;
947 	    continue;
948 	}
949 
950 	/* Now we point q at a string which looks like
951 	   "edit+unedit+commit,"... and walk down it.  */
952 	q = endp + 1;
953 	notif = NULL;
954 	while (q != NULL)
955 	{
956 	    endq = strchr (q, '+');
957 	    if (endq == NULL || (nextp != NULL && endq > nextp))
958 	    {
959 		if (nextp == NULL)
960 		    endq = q + strlen (q);
961 		else
962 		    endq = nextp;
963 		nextq = NULL;
964 	    }
965 	    else
966 		nextq = endq + 1;
967 
968 	    /* If there is a temporary and a regular watch, send a single
969 	       notification, for the regular watch.  */
970 	    if (type == 'E' && endq - q == 4 && strncmp ("edit", q, 4) == 0)
971 	    {
972 		notif = "edit";
973 	    }
974 	    else if (type == 'U'
975 		     && endq - q == 6 && strncmp ("unedit", q, 6) == 0)
976 	    {
977 		notif = "unedit";
978 	    }
979 	    else if (type == 'C'
980 		     && endq - q == 6 && strncmp ("commit", q, 6) == 0)
981 	    {
982 		notif = "commit";
983 	    }
984 	    else if (type == 'E'
985 		     && endq - q == 5 && strncmp ("tedit", q, 5) == 0)
986 	    {
987 		if (notif == NULL)
988 		    notif = "temporary edit";
989 	    }
990 	    else if (type == 'U'
991 		     && endq - q == 7 && strncmp ("tunedit", q, 7) == 0)
992 	    {
993 		if (notif == NULL)
994 		    notif = "temporary unedit";
995 	    }
996 	    else if (type == 'C'
997 		     && endq - q == 7 && strncmp ("tcommit", q, 7) == 0)
998 	    {
999 		if (notif == NULL)
1000 		    notif = "temporary commit";
1001 	    }
1002 	    q = nextq;
1003 	}
1004 	if (nextp != NULL)
1005 	    ++nextp;
1006 
1007 	if (notif != NULL)
1008 	{
1009 	    struct notify_proc_args args;
1010 	    size_t len = endp - p;
1011 	    FILE *fp;
1012 	    char *usersname;
1013 	    char *line = NULL;
1014 	    size_t line_len = 0;
1015 
1016 	    args.notifyee = NULL;
1017 	    usersname = Xasprintf ("%s/%s/%s", current_parsed_root->directory,
1018 				   CVSROOTADM, CVSROOTADM_USERS);
1019 	    fp = CVS_FOPEN (usersname, "r");
1020 	    if (fp == NULL && !existence_error (errno))
1021 		error (0, errno, "cannot read %s", usersname);
1022 	    if (fp != NULL)
1023 	    {
1024 		while (getline (&line, &line_len, fp) >= 0)
1025 		{
1026 		    if (strncmp (line, p, len) == 0
1027 			&& line[len] == ':')
1028 		    {
1029 			char *cp;
1030 			args.notifyee = xstrdup (line + len + 1);
1031 
1032                         /* There may or may not be more
1033                            colon-separated fields added to this in the
1034                            future; in any case, we ignore them right
1035                            now, and if there are none we make sure to
1036                            chop off the final newline, if any. */
1037 			cp = strpbrk (args.notifyee, ":\n");
1038 
1039 			if (cp != NULL)
1040 			    *cp = '\0';
1041 			break;
1042 		    }
1043 		}
1044 		if (ferror (fp))
1045 		    error (0, errno, "cannot read %s", usersname);
1046 		if (fclose (fp) < 0)
1047 		    error (0, errno, "cannot close %s", usersname);
1048 	    }
1049 	    free (usersname);
1050 	    if (line != NULL)
1051 		free (line);
1052 
1053 	    if (args.notifyee == NULL)
1054 	    {
1055 		char *tmp;
1056 		tmp = xmalloc (endp - p + 1);
1057 		strncpy (tmp, p, endp - p);
1058 		tmp[endp - p] = '\0';
1059 		args.notifyee = tmp;
1060 	    }
1061 
1062 	    args.type = notif;
1063 	    args.who = who;
1064 	    args.file = filename;
1065 
1066 	    (void) Parse_Info (CVSROOTADM_NOTIFY, repository, notify_proc,
1067 			       PIOPT_ALL, &args);
1068             /* It's okay to cast out the const for the free() below since we
1069              * just allocated this a few lines above.  The const was for
1070              * everybody else.
1071              */
1072 	    free ((char *)args.notifyee);
1073 	}
1074 
1075 	p = nextp;
1076     }
1077     if (watchers != NULL)
1078 	free (watchers);
1079 
1080     switch (type)
1081     {
1082 	case 'E':
1083 	    if (*watches == 'E')
1084 	    {
1085 		args.add_tedit = 1;
1086 		++watches;
1087 	    }
1088 	    if (*watches == 'U')
1089 	    {
1090 		args.add_tunedit = 1;
1091 		++watches;
1092 	    }
1093 	    if (*watches == 'C')
1094 	    {
1095 		args.add_tcommit = 1;
1096 	    }
1097 	    watch_modify_watchers (filename, &args);
1098 	    break;
1099 	case 'U':
1100 	case 'C':
1101 	    args.remove_temp = 1;
1102 	    watch_modify_watchers (filename, &args);
1103 	    break;
1104     }
1105 }
1106 
1107 
1108 
1109 #ifdef CLIENT_SUPPORT
1110 /* Check and send notifications.  This is only for the client.  */
1111 void
1112 notify_check (const char *repository, const char *update_dir)
1113 {
1114     FILE *fp;
1115     char *line = NULL;
1116     size_t line_len = 0;
1117 
1118     if (!server_started)
1119 	/* We are in the midst of a command which is not to talk to
1120 	   the server (e.g. the first phase of a cvs edit).  Just chill
1121 	   out, we'll catch the notifications on the flip side.  */
1122 	return;
1123 
1124     /* We send notifications even if noexec.  I'm not sure which behavior
1125        is most sensible.  */
1126 
1127     fp = CVS_FOPEN (CVSADM_NOTIFY, "r");
1128     if (fp == NULL)
1129     {
1130 	if (!existence_error (errno))
1131 	    error (0, errno, "cannot open %s", CVSADM_NOTIFY);
1132 	return;
1133     }
1134     while (getline (&line, &line_len, fp) > 0)
1135     {
1136 	int notif_type;
1137 	char *filename;
1138 	char *val;
1139 	char *cp;
1140 
1141 	notif_type = line[0];
1142 	if (notif_type == '\0')
1143 	    continue;
1144 	filename = line + 1;
1145 	cp = strchr (filename, '\t');
1146 	if (cp == NULL)
1147 	    continue;
1148 	*cp++ = '\0';
1149 	val = cp;
1150 
1151 	client_notify (repository, update_dir, filename, notif_type, val);
1152     }
1153     if (line)
1154 	free (line);
1155     if (ferror (fp))
1156 	error (0, errno, "cannot read %s", CVSADM_NOTIFY);
1157     if (fclose (fp) < 0)
1158 	error (0, errno, "cannot close %s", CVSADM_NOTIFY);
1159 
1160     /* Leave the CVSADM_NOTIFY file there, until the server tells us it
1161        has dealt with it.  */
1162 }
1163 #endif /* CLIENT_SUPPORT */
1164 
1165 
1166 static const char *const editors_usage[] =
1167 {
1168     "Usage: %s %s [-lR] [<file>]...\n",
1169     "-l\tProcess this directory only (not recursive).\n",
1170     "-R\tProcess directories recursively (default).\n",
1171     "(Specify the --help global option for a list of other help options.)\n",
1172     NULL
1173 };
1174 
1175 
1176 
1177 static int
1178 editors_fileproc (void *callerdat, struct file_info *finfo)
1179 {
1180     return find_editors_and_output (finfo);
1181 }
1182 
1183 
1184 
1185 int
1186 editors (int argc, char **argv)
1187 {
1188     int local = 0;
1189     int c;
1190 
1191     if (argc == -1)
1192 	usage (editors_usage);
1193 
1194     optind = 0;
1195     while ((c = getopt (argc, argv, "+lR")) != -1)
1196     {
1197 	switch (c)
1198 	{
1199 	    case 'l':
1200 		local = 1;
1201 		break;
1202 	    case 'R':
1203 		local = 0;
1204 		break;
1205 	    case '?':
1206 	    default:
1207 		usage (editors_usage);
1208 		break;
1209 	}
1210     }
1211     argc -= optind;
1212     argv += optind;
1213 
1214 #ifdef CLIENT_SUPPORT
1215     if (current_parsed_root->isremote)
1216     {
1217 	start_server ();
1218 	ign_setup ();
1219 
1220 	if (local)
1221 	    send_arg ("-l");
1222 	send_arg ("--");
1223 	send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
1224 	send_file_names (argc, argv, SEND_EXPAND_WILD);
1225 	send_to_server ("editors\012", 0);
1226 	return get_responses_and_close ();
1227     }
1228 #endif /* CLIENT_SUPPORT */
1229 
1230     return start_recursion (editors_fileproc, NULL, NULL, NULL, NULL,
1231 			    argc, argv, local, W_LOCAL, 0, CVS_LOCK_READ, NULL,
1232 			    0, NULL);
1233 }
1234