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