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