edit.c   [plain text]


/* Implementation for "cvs edit", "cvs watch on", and related commands

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.  */

#include "cvs.h"
#include "getline.h"
#include "watch.h"
#include "edit.h"
#include "fileattr.h"

static int watch_onoff PROTO ((int, char **));

static int setting_default;
static int turning_on;

static int setting_tedit;
static int setting_tunedit;
static int setting_tcommit;

static int onoff_fileproc PROTO ((void *callerdat, struct file_info *finfo));

static int
onoff_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    fileattr_set (finfo->file, "_watched", turning_on ? "" : NULL);
    return 0;
}

static int onoff_filesdoneproc PROTO ((void *, int, char *, char *, List *));

static int
onoff_filesdoneproc (callerdat, err, repository, update_dir, entries)
    void *callerdat;
    int err;
    char *repository;
    char *update_dir;
    List *entries;
{
    if (setting_default)
	fileattr_set (NULL, "_watched", turning_on ? "" : NULL);
    return err;
}

static int
watch_onoff (argc, argv)
    int argc;
    char **argv;
{
    int c;
    int local = 0;
    int err;

    optind = 0;
    while ((c = getopt (argc, argv, "+lR")) != -1)
    {
	switch (c)
	{
	    case 'l':
		local = 1;
		break;
	    case 'R':
		local = 0;
		break;
	    case '?':
	    default:
		usage (watch_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

#ifdef CLIENT_SUPPORT
    if (client_active)
    {
	start_server ();

	ign_setup ();

	if (local)
	    send_arg ("-l");
	send_file_names (argc, argv, SEND_EXPAND_WILD);
	send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
	send_to_server (turning_on ? "watch-on\012" : "watch-off\012", 0);
	return get_responses_and_close ();
    }
#endif /* CLIENT_SUPPORT */

    setting_default = (argc <= 0);

    lock_tree_for_write (argc, argv, local, 0);

    err = start_recursion (onoff_fileproc, onoff_filesdoneproc,
			   (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
			   argc, argv, local, W_LOCAL, 0, 0, (char *)NULL,
			   0);

    Lock_Cleanup ();
    return err;
}

int
watch_on (argc, argv)
    int argc;
    char **argv;
{
    turning_on = 1;
    return watch_onoff (argc, argv);
}

int
watch_off (argc, argv)
    int argc;
    char **argv;
{
    turning_on = 0;
    return watch_onoff (argc, argv);
}

static int dummy_fileproc PROTO ((void *callerdat, struct file_info *finfo));

static int
dummy_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    /* This is a pretty hideous hack, but the gist of it is that recurse.c
       won't call notify_check unless there is a fileproc, so we can't just
       pass NULL for fileproc.  */
    return 0;
}

static int ncheck_fileproc PROTO ((void *callerdat, struct file_info *finfo));

/* Check for and process notifications.  Local only.  I think that doing
   this as a fileproc is the only way to catch all the
   cases (e.g. foo/bar.c), even though that means checking over and over
   for the same CVSADM_NOTIFY file which we removed the first time we
   processed the directory.  */

static int
ncheck_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    int notif_type;
    char *filename;
    char *val;
    char *cp;
    char *watches;

    FILE *fp;
    char *line = NULL;
    size_t line_len = 0;

    /* We send notifications even if noexec.  I'm not sure which behavior
       is most sensible.  */

    fp = CVS_FOPEN (CVSADM_NOTIFY, "r");
    if (fp == NULL)
    {
	if (!existence_error (errno))
	    error (0, errno, "cannot open %s", CVSADM_NOTIFY);
	return 0;
    }

    while (getline (&line, &line_len, fp) > 0)
    {
	notif_type = line[0];
	if (notif_type == '\0')
	    continue;
	filename = line + 1;
	cp = strchr (filename, '\t');
	if (cp == NULL)
	    continue;
	*cp++ = '\0';
	val = cp;
	cp = strchr (val, '\t');
	if (cp == NULL)
	    continue;
	*cp++ = '+';
	cp = strchr (cp, '\t');
	if (cp == NULL)
	    continue;
	*cp++ = '+';
	cp = strchr (cp, '\t');
	if (cp == NULL)
	    continue;
	*cp++ = '\0';
	watches = cp;
	cp = strchr (cp, '\n');
	if (cp == NULL)
	    continue;
	*cp = '\0';

	notify_do (notif_type, filename, getcaller (), val, watches,
		   finfo->repository);
    }
    free (line);

    if (ferror (fp))
	error (0, errno, "cannot read %s", CVSADM_NOTIFY);
    if (fclose (fp) < 0)
	error (0, errno, "cannot close %s", CVSADM_NOTIFY);

    if ( CVS_UNLINK (CVSADM_NOTIFY) < 0)
	error (0, errno, "cannot remove %s", CVSADM_NOTIFY);

    return 0;
}

static int send_notifications PROTO ((int, char **, int));

/* Look through the CVSADM_NOTIFY file and process each item there
   accordingly.  */
static int
send_notifications (argc, argv, local)
    int argc;
    char **argv;
    int local;
{
    int err = 0;

#ifdef CLIENT_SUPPORT
    /* OK, we've done everything which needs to happen on the client side.
       Now we can try to contact the server; if we fail, then the
       notifications stay in CVSADM_NOTIFY to be sent next time.  */
    if (client_active)
    {
	if (strcmp (command_name, "release") != 0)
	{
	    start_server ();
	    ign_setup ();
	}

	err += start_recursion (dummy_fileproc, (FILESDONEPROC) NULL,
				(DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
				argc, argv, local, W_LOCAL, 0, 0, (char *)NULL,
				0);

	send_to_server ("noop\012", 0);
	if (strcmp (command_name, "release") == 0)
	    err += get_server_responses ();
	else
	    err += get_responses_and_close ();
    }
    else
#endif
    {
	/* Local.  */

	lock_tree_for_write (argc, argv, local, 0);
	err += start_recursion (ncheck_fileproc, (FILESDONEPROC) NULL,
				(DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
				argc, argv, local, W_LOCAL, 0, 0, (char *)NULL,
				0);
	Lock_Cleanup ();
    }
    return err;
}

static int edit_fileproc PROTO ((void *callerdat, struct file_info *finfo));

static int
edit_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    FILE *fp;
    time_t now;
    char *ascnow;
    char *basefilename;

    if (noexec)
	return 0;

    /* This is a somewhat screwy way to check for this, because it
       doesn't help errors other than the nonexistence of the file
       (e.g. permissions problems).  It might be better to rearrange
       the code so that CVSADM_NOTIFY gets written only after the
       various actions succeed (but what if only some of them
       succeed).  */
    if (!isfile (finfo->file))
    {
	error (0, 0, "no such file %s; ignored", finfo->fullname);
	return 0;
    }

    fp = open_file (CVSADM_NOTIFY, "a");

    (void) time (&now);
    ascnow = asctime (gmtime (&now));
    ascnow[24] = '\0';
    fprintf (fp, "E%s\t%s GMT\t%s\t%s\t", finfo->file,
	     ascnow, hostname, CurDir);
    if (setting_tedit)
	fprintf (fp, "E");
    if (setting_tunedit)
	fprintf (fp, "U");
    if (setting_tcommit)
	fprintf (fp, "C");
    fprintf (fp, "\n");

    if (fclose (fp) < 0)
    {
	if (finfo->update_dir[0] == '\0')
	    error (0, errno, "cannot close %s", CVSADM_NOTIFY);
	else
	    error (0, errno, "cannot close %s/%s", finfo->update_dir,
		   CVSADM_NOTIFY);
    }

    xchmod (finfo->file, 1);

    /* Now stash the file away in CVSADM so that unedit can revert even if
       it can't communicate with the server.  We stash away a writable
       copy so that if the user removes the working file, then restores it
       with "cvs update" (which clears _editors but does not update
       CVSADM_BASE), then a future "cvs edit" can still win.  */
    /* Could save a system call by only calling mkdir_if_needed if
       trying to create the output file fails.  But copy_file isn't
       set up to facilitate that.  */
    mkdir_if_needed (CVSADM_BASE);
    basefilename = xmalloc (10 + sizeof CVSADM_BASE + strlen (finfo->file));
    strcpy (basefilename, CVSADM_BASE);
    strcat (basefilename, "/");
    strcat (basefilename, finfo->file);
    copy_file (finfo->file, basefilename);
    free (basefilename);

    {
	Node *node;

	node = findnode_fn (finfo->entries, finfo->file);
	if (node != NULL)
	    base_register (finfo, ((Entnode *) node->data)->version);
    }

    return 0;
}

static const char *const edit_usage[] =
{
    "Usage: %s %s [-lR] [files...]\n",
    "-l: Local directory only, not recursive\n",
    "-R: Process directories recursively\n",
    "-a: Specify what actions for temporary watch, one of\n",
    "    edit,unedit,commit,all,none\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

int
edit (argc, argv)
    int argc;
    char **argv;
{
    int local = 0;
    int c;
    int err;
    int a_omitted;

    if (argc == -1)
	usage (edit_usage);

    a_omitted = 1;
    setting_tedit = 0;
    setting_tunedit = 0;
    setting_tcommit = 0;
    optind = 0;
    while ((c = getopt (argc, argv, "+lRa:")) != -1)
    {
	switch (c)
	{
	    case 'l':
		local = 1;
		break;
	    case 'R':
		local = 0;
		break;
	    case 'a':
		a_omitted = 0;
		if (strcmp (optarg, "edit") == 0)
		    setting_tedit = 1;
		else if (strcmp (optarg, "unedit") == 0)
		    setting_tunedit = 1;
		else if (strcmp (optarg, "commit") == 0)
		    setting_tcommit = 1;
		else if (strcmp (optarg, "all") == 0)
		{
		    setting_tedit = 1;
		    setting_tunedit = 1;
		    setting_tcommit = 1;
		}
		else if (strcmp (optarg, "none") == 0)
		{
		    setting_tedit = 0;
		    setting_tunedit = 0;
		    setting_tcommit = 0;
		}
		else
		    usage (edit_usage);
		break;
	    case '?':
	    default:
		usage (edit_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    if (a_omitted)
    {
	setting_tedit = 1;
	setting_tunedit = 1;
	setting_tcommit = 1;
    }

    /* No need to readlock since we aren't doing anything to the
       repository.  */
    err = start_recursion (edit_fileproc, (FILESDONEPROC) NULL,
			   (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
			   argc, argv, local, W_LOCAL, 0, 0, (char *)NULL,
			   0);

    err += send_notifications (argc, argv, local);

    return err;
}

static int unedit_fileproc PROTO ((void *callerdat, struct file_info *finfo));

static int
unedit_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    FILE *fp;
    time_t now;
    char *ascnow;
    char *basefilename;

    if (noexec)
	return 0;

    basefilename = xmalloc (10 + sizeof CVSADM_BASE + strlen (finfo->file));
    strcpy (basefilename, CVSADM_BASE);
    strcat (basefilename, "/");
    strcat (basefilename, finfo->file);
    if (!isfile (basefilename))
    {
	/* This file apparently was never cvs edit'd (e.g. we are uneditting
	   a directory where only some of the files were cvs edit'd.  */
	free (basefilename);
	return 0;
    }

    if (xcmp (finfo->file, basefilename) != 0)
    {
	printf ("%s has been modified; revert changes? ", finfo->fullname);
	if (!yesno ())
	{
	    /* "no".  */
	    free (basefilename);
	    return 0;
	}
    }
    rename_file (basefilename, finfo->file);
    free (basefilename);

    fp = open_file (CVSADM_NOTIFY, "a");

    (void) time (&now);
    ascnow = asctime (gmtime (&now));
    ascnow[24] = '\0';
    fprintf (fp, "U%s\t%s GMT\t%s\t%s\t\n", finfo->file,
	     ascnow, hostname, CurDir);

    if (fclose (fp) < 0)
    {
	if (finfo->update_dir[0] == '\0')
	    error (0, errno, "cannot close %s", CVSADM_NOTIFY);
	else
	    error (0, errno, "cannot close %s/%s", finfo->update_dir,
		   CVSADM_NOTIFY);
    }

    /* Now update the revision number in CVS/Entries from CVS/Baserev.
       The basic idea here is that we are reverting to the revision
       that the user edited.  If we wanted "cvs update" to update
       CVS/Base as we go along (so that an unedit could revert to the
       current repository revision), we would need:

       update (or all send_files?) (client) needs to send revision in
       new Entry-base request.  update (server/local) needs to check
       revision against repository and send new Update-base response
       (like Update-existing in that the file already exists.  While
       we are at it, might try to clean up the syntax by having the
       mode only in a "Mode" response, not in the Update-base itself).  */
    {
	char *baserev;
	Node *node;
	Entnode *entdata;

	baserev = base_get (finfo);
	node = findnode_fn (finfo->entries, finfo->file);
	/* The case where node is NULL probably should be an error or
	   something, but I don't want to think about it too hard right
	   now.  */
	if (node != NULL)
	{
	    entdata = (Entnode *) node->data;
	    if (baserev == NULL)
	    {
		/* This can only happen if the CVS/Baserev file got
		   corrupted.  We suspect it might be possible if the
		   user interrupts CVS, although I haven't verified
		   that.  */
		error (0, 0, "%s not mentioned in %s", finfo->fullname,
		       CVSADM_BASEREV);

		/* Since we don't know what revision the file derives from,
		   keeping it around would be asking for trouble.  */
		if (unlink_file (finfo->file) < 0)
		    error (0, errno, "cannot remove %s", finfo->fullname);

		/* This is cheesy, in a sense; why shouldn't we do the
		   update for the user?  However, doing that would require
		   contacting the server, so maybe this is OK.  */
		error (0, 0, "run update to complete the unedit");
		return 0;
	    }
	    Register (finfo->entries, finfo->file, baserev, entdata->timestamp,
		      entdata->options, entdata->tag, entdata->date,
		      entdata->conflict);
	}
	free (baserev);
	base_deregister (finfo);
    }

    xchmod (finfo->file, 0);
    return 0;
}

int
unedit (argc, argv)
    int argc;
    char **argv;
{
    int local = 0;
    int c;
    int err;

    if (argc == -1)
	usage (edit_usage);

    optind = 0;
    while ((c = getopt (argc, argv, "+lR")) != -1)
    {
	switch (c)
	{
	    case 'l':
		local = 1;
		break;
	    case 'R':
		local = 0;
		break;
	    case '?':
	    default:
		usage (edit_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    /* No need to readlock since we aren't doing anything to the
       repository.  */
    err = start_recursion (unedit_fileproc, (FILESDONEPROC) NULL,
			   (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
			   argc, argv, local, W_LOCAL, 0, 0, (char *)NULL,
			   0);

    err += send_notifications (argc, argv, local);

    return err;
}

void
mark_up_to_date (file)
    char *file;
{
    char *base;

    /* The file is up to date, so we better get rid of an out of
       date file in CVSADM_BASE.  */
    base = xmalloc (strlen (file) + 80);
    strcpy (base, CVSADM_BASE);
    strcat (base, "/");
    strcat (base, file);
    if (unlink_file (base) < 0 && ! existence_error (errno))
	error (0, errno, "cannot remove %s", file);
    free (base);
}


void
editor_set (filename, editor, val)
    char *filename;
    char *editor;
    char *val;
{
    char *edlist;
    char *newlist;

    edlist = fileattr_get0 (filename, "_editors");
    newlist = fileattr_modify (edlist, editor, val, '>', ',');
    /* If the attributes is unchanged, don't rewrite the attribute file.  */
    if (!((edlist == NULL && newlist == NULL)
	  || (edlist != NULL
	      && newlist != NULL
	      && strcmp (edlist, newlist) == 0)))
	fileattr_set (filename, "_editors", newlist);
    if (edlist != NULL)
	free (edlist);
    if (newlist != NULL)
	free (newlist);
}

struct notify_proc_args {
    /* What kind of notification, "edit", "tedit", etc.  */
    char *type;
    /* User who is running the command which causes notification.  */
    char *who;
    /* User to be notified.  */
    char *notifyee;
    /* File.  */
    char *file;
};

/* Pass as a static until we get around to fixing Parse_Info to pass along
   a void * where we can stash it.  */
static struct notify_proc_args *notify_args;

static int notify_proc PROTO ((char *repository, char *filter));

static int
notify_proc (repository, filter)
    char *repository;
    char *filter;
{
    FILE *pipefp;
    char *prog;
    char *expanded_prog;
    char *p;
    char *q;
    char *srepos;
    struct notify_proc_args *args = notify_args;

    srepos = Short_Repository (repository);
    prog = xmalloc (strlen (filter) + strlen (args->notifyee) + 1);
    /* Copy FILTER to PROG, replacing the first occurrence of %s with
       the notifyee.  We only allocated enough memory for one %s, and I doubt
       there is a need for more.  */
    for (p = filter, q = prog; *p != '\0'; ++p)
    {
	if (p[0] == '%')
	{
	    if (p[1] == 's')
	    {
		strcpy (q, args->notifyee);
		q += strlen (q);
		strcpy (q, p + 2);
		q += strlen (q);
		break;
	    }
	    else
		continue;
	}
	*q++ = *p;
    }
    *q = '\0';

    /* FIXME: why are we calling expand_proc?  Didn't we already
       expand it in Parse_Info, before passing it to notify_proc?  */
    expanded_prog = expand_path (prog, "notify", 0);
    if (!expanded_prog)
    {
	free (prog);
	return 1;
    }

    pipefp = run_popen (expanded_prog, "w");
    if (pipefp == NULL)
    {
	error (0, errno, "cannot write entry to notify filter: %s", prog);
	free (prog);
	free (expanded_prog);
	return 1;
    }

    fprintf (pipefp, "%s %s\n---\n", srepos, args->file);
    fprintf (pipefp, "Triggered %s watch on %s\n", args->type, repository);
    fprintf (pipefp, "By %s\n", args->who);

    /* Lots more potentially useful information we could add here; see
       logfile_write for inspiration.  */

    free (prog);
    free (expanded_prog);
    return (pclose (pipefp));
}

void
notify_do (type, filename, who, val, watches, repository)
    int type;
    char *filename;
    char *who;
    char *val;
    char *watches;
    char *repository;
{
    static struct addremove_args blank;
    struct addremove_args args;
    char *watchers;
    char *p;
    char *endp;
    char *nextp;

    /* Initialize fields to 0, NULL, or 0.0.  */
    args = blank;
    switch (type)
    {
	case 'E':
	    editor_set (filename, who, val);
	    break;
	case 'U':
	case 'C':
	    editor_set (filename, who, NULL);
	    break;
	default:
	    return;
    }

    watchers = fileattr_get0 (filename, "_watchers");
    p = watchers;
    while (p != NULL)
    {
	char *q;
	char *endq;
	char *nextq;
	char *notif;

	endp = strchr (p, '>');
	if (endp == NULL)
	    break;
	nextp = strchr (p, ',');

	if ((size_t)(endp - p) == strlen (who) && strncmp (who, p, endp - p) == 0)
	{
	    /* Don't notify user of their own changes.  Would perhaps
	       be better to check whether it is the same working
	       directory, not the same user, but that is hairy.  */
	    p = nextp == NULL ? nextp : nextp + 1;
	    continue;
	}

	/* Now we point q at a string which looks like
	   "edit+unedit+commit,"... and walk down it.  */
	q = endp + 1;
	notif = NULL;
	while (q != NULL)
	{
	    endq = strchr (q, '+');
	    if (endq == NULL || (nextp != NULL && endq > nextp))
	    {
		if (nextp == NULL)
		    endq = q + strlen (q);
		else
		    endq = nextp;
		nextq = NULL;
	    }
	    else
		nextq = endq + 1;

	    /* If there is a temporary and a regular watch, send a single
	       notification, for the regular watch.  */
	    if (type == 'E' && endq - q == 4 && strncmp ("edit", q, 4) == 0)
	    {
		notif = "edit";
	    }
	    else if (type == 'U'
		     && endq - q == 6 && strncmp ("unedit", q, 6) == 0)
	    {
		notif = "unedit";
	    }
	    else if (type == 'C'
		     && endq - q == 6 && strncmp ("commit", q, 6) == 0)
	    {
		notif = "commit";
	    }
	    else if (type == 'E'
		     && endq - q == 5 && strncmp ("tedit", q, 5) == 0)
	    {
		if (notif == NULL)
		    notif = "temporary edit";
	    }
	    else if (type == 'U'
		     && endq - q == 7 && strncmp ("tunedit", q, 7) == 0)
	    {
		if (notif == NULL)
		    notif = "temporary unedit";
	    }
	    else if (type == 'C'
		     && endq - q == 7 && strncmp ("tcommit", q, 7) == 0)
	    {
		if (notif == NULL)
		    notif = "temporary commit";
	    }
	    q = nextq;
	}
	if (nextp != NULL)
	    ++nextp;

	if (notif != NULL)
	{
	    struct notify_proc_args args;
	    size_t len = endp - p;
	    FILE *fp;
	    char *usersname;
	    char *line = NULL;
	    size_t line_len = 0;

	    args.notifyee = NULL;
	    usersname = xmalloc (strlen (CVSroot_directory)
				 + sizeof CVSROOTADM
				 + sizeof CVSROOTADM_USERS
				 + 20);
	    strcpy (usersname, CVSroot_directory);
	    strcat (usersname, "/");
	    strcat (usersname, CVSROOTADM);
	    strcat (usersname, "/");
	    strcat (usersname, CVSROOTADM_USERS);
	    fp = CVS_FOPEN (usersname, "r");
	    if (fp == NULL && !existence_error (errno))
		error (0, errno, "cannot read %s", usersname);
	    if (fp != NULL)
	    {
		while (getline (&line, &line_len, fp) >= 0)
		{
		    if (strncmp (line, p, len) == 0
			&& line[len] == ':')
		    {
			char *cp;
			args.notifyee = xstrdup (line + len + 1);
			cp = strchr (args.notifyee, ':');
			if (cp != NULL)
			    *cp = '\0';
			break;
		    }
		}
		if (ferror (fp))
		    error (0, errno, "cannot read %s", usersname);
		if (fclose (fp) < 0)
		    error (0, errno, "cannot close %s", usersname);
	    }
	    free (usersname);
	    free (line);

	    if (args.notifyee == NULL)
	    {
		args.notifyee = xmalloc (endp - p + 1);
		strncpy (args.notifyee, p, endp - p);
		args.notifyee[endp - p] = '\0';
	    }

	    notify_args = &args;
	    args.type = notif;
	    args.who = who;
	    args.file = filename;

	    (void) Parse_Info (CVSROOTADM_NOTIFY, repository, notify_proc, 1);
	    free (args.notifyee);
	}

	p = nextp;
    }
    if (watchers != NULL)
	free (watchers);

    switch (type)
    {
	case 'E':
	    if (*watches == 'E')
	    {
		args.add_tedit = 1;
		++watches;
	    }
	    if (*watches == 'U')
	    {
		args.add_tunedit = 1;
		++watches;
	    }
	    if (*watches == 'C')
	    {
		args.add_tcommit = 1;
	    }
	    watch_modify_watchers (filename, &args);
	    break;
	case 'U':
	case 'C':
	    args.remove_temp = 1;
	    watch_modify_watchers (filename, &args);
	    break;
    }
}

#ifdef CLIENT_SUPPORT
/* Check and send notifications.  This is only for the client.  */
void
notify_check (repository, update_dir)
    char *repository;
    char *update_dir;
{
    FILE *fp;
    char *line = NULL;
    size_t line_len = 0;

    if (! server_started)
	/* We are in the midst of a command which is not to talk to
	   the server (e.g. the first phase of a cvs edit).  Just chill
	   out, we'll catch the notifications on the flip side.  */
	return;

    /* We send notifications even if noexec.  I'm not sure which behavior
       is most sensible.  */

    fp = CVS_FOPEN (CVSADM_NOTIFY, "r");
    if (fp == NULL)
    {
	if (!existence_error (errno))
	    error (0, errno, "cannot open %s", CVSADM_NOTIFY);
	return;
    }
    while (getline (&line, &line_len, fp) > 0)
    {
	int notif_type;
	char *filename;
	char *val;
	char *cp;

	notif_type = line[0];
	if (notif_type == '\0')
	    continue;
	filename = line + 1;
	cp = strchr (filename, '\t');
	if (cp == NULL)
	    continue;
	*cp++ = '\0';
	val = cp;

	client_notify (repository, update_dir, filename, notif_type, val);
    }
    if (line)
	free (line);
    if (ferror (fp))
	error (0, errno, "cannot read %s", CVSADM_NOTIFY);
    if (fclose (fp) < 0)
	error (0, errno, "cannot close %s", CVSADM_NOTIFY);

    /* Leave the CVSADM_NOTIFY file there, until the server tells us it
       has dealt with it.  */
}
#endif /* CLIENT_SUPPORT */


static const char *const editors_usage[] =
{
    "Usage: %s %s [-lR] [files...]\n",
    "\t-l\tProcess this directory only (not recursive).\n",
    "\t-R\tProcess directories recursively.\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

static int editors_fileproc PROTO ((void *callerdat, struct file_info *finfo));

static int
editors_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    char *them;
    char *p;

    them = fileattr_get0 (finfo->file, "_editors");
    if (them == NULL)
	return 0;

    fputs (finfo->fullname, stdout);

    p = them;
    while (1)
    {
	putc ('\t', stdout);
	while (*p != '>' && *p != '\0')
	    putc (*p++, stdout);
	if (*p == '\0')
	{
	    /* Only happens if attribute is misformed.  */
	    putc ('\n', stdout);
	    break;
	}
	++p;
	putc ('\t', stdout);
	while (1)
	{
	    while (*p != '+' && *p != ',' && *p != '\0')
		putc (*p++, stdout);
	    if (*p == '\0')
	    {
		putc ('\n', stdout);
		goto out;
	    }
	    if (*p == ',')
	    {
		++p;
		break;
	    }
	    ++p;
	    putc ('\t', stdout);
	}
	putc ('\n', stdout);
    }
  out:;
    return 0;
}

int
editors (argc, argv)
    int argc;
    char **argv;
{
    int local = 0;
    int c;

    if (argc == -1)
	usage (editors_usage);

    optind = 0;
    while ((c = getopt (argc, argv, "+lR")) != -1)
    {
	switch (c)
	{
	    case 'l':
		local = 1;
		break;
	    case 'R':
		local = 0;
		break;
	    case '?':
	    default:
		usage (editors_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

#ifdef CLIENT_SUPPORT
    if (client_active)
    {
	start_server ();
	ign_setup ();

	if (local)
	    send_arg ("-l");
	send_file_names (argc, argv, SEND_EXPAND_WILD);
	send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
	send_to_server ("editors\012", 0);
	return get_responses_and_close ();
    }
#endif /* CLIENT_SUPPORT */

    return start_recursion (editors_fileproc, (FILESDONEPROC) NULL,
			    (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL,
			    argc, argv, local, W_LOCAL, 0, 1, (char *)NULL,
			    0);
}