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