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