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