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