/* 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; { char *watched = fileattr_get0 (finfo->file, "_watched"); fileattr_set (finfo->file, "_watched", turning_on ? "" : NULL); if (watched != NULL) free (watched); return 0; } static int onoff_filesdoneproc PROTO ((void *, int, const char *, const char *, List *)); static int onoff_filesdoneproc (callerdat, err, repository, update_dir, entries) void *callerdat; int err; const char *repository; const char *update_dir; List *entries; { if (setting_default) { char *watched = fileattr_get0 (NULL, "_watched"); fileattr_set (NULL, "_watched", turning_on ? "" : NULL); if (watched != NULL) free (watched); } 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 (current_parsed_root->isremote) { start_server (); ign_setup (); if (local) send_arg ("-l"); send_arg ("--"); send_files (argc, argv, local, 0, SEND_NO_CONTENTS); send_file_names (argc, argv, SEND_EXPAND_WILD); 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, W_LOCAL, 0); err = start_recursion (onoff_fileproc, onoff_filesdoneproc, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, CVS_LOCK_NONE, (char *) NULL, 0, (char *) NULL); 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 cvs_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 (current_parsed_root->isremote) { if (strcmp (cvs_cmd_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, (char *) NULL); send_to_server ("noop\012", 0); if (strcmp (cvs_cmd_name, "release") == 0) err += get_server_responses (); else err += get_responses_and_close (); } else #endif { /* Local. */ lock_tree_for_write (argc, argv, local, W_LOCAL, 0); err += start_recursion (ncheck_fileproc, (FILESDONEPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, 0, (char *) NULL); 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'; /* Fix non-standard format. */ if (ascnow[8] == '0') ascnow[8] = ' '; 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] [-a ]... []...\n", "-l\tLocal directory only, not recursive.\n", "-R\tProcess directories recursively (default).\n", "-a\tSpecify action to register for temporary watch, one of:\n", " \t`edit', `unedit', `commit', `all', or `none' (defaults to `all').\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; } if (strpbrk (hostname, "+,>;=\t\n") != NULL) error (1, 0, "host name (%s) contains an invalid character (+,>;=\\t\\n)", hostname); if (strpbrk (CurDir, "+,>;=\t\n") != NULL) error (1, 0, "current directory (%s) contains an invalid character (+,>;=\\t\\n)", CurDir); /* 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, (char *) NULL); 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'; /* Fix non-standard format. */ if (ascnow[8] == '0') ascnow[8] = ' '; 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 = 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; } static const char *const unedit_usage[] = { "Usage: %s %s [-lR] []...\n", "-l\tLocal directory only, not recursive.\n", "-R\tProcess directories recursively (default).\n", "(Specify the --help global option for a list of other help options.)\n", NULL }; int unedit (argc, argv) int argc; char **argv; { int local = 0; int c; int err; if (argc == -1) usage (unedit_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 (unedit_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, (char *) NULL); err += send_notifications (argc, argv, local); return err; } void mark_up_to_date (file) const 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) const char *filename; const char *editor; const 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. */ const char *type; /* User who is running the command which causes notification. */ const char *who; /* User to be notified. */ const char *notifyee; /* File. */ const 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 ((const char *repository, const char *filter)); static int notify_proc (repository, filter) const char *repository; const char *filter; { FILE *pipefp; char *prog; char *expanded_prog; const char *p; char *q; const 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)); } /* FIXME: this function should have a way to report whether there was an error so that server.c can know whether to report Notified back to the client. */ void notify_do (type, filename, who, val, watches, repository) int type; const char *filename; const char *who; const char *val; const char *watches; const 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': if (strpbrk (val, ",>;=\n") != NULL) { error (0, 0, "invalid character in editor value"); return; } 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 (current_parsed_root->directory) + sizeof CVSROOTADM + sizeof CVSROOTADM_USERS + 20); strcpy (usersname, current_parsed_root->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); /* There may or may not be more colon-separated fields added to this in the future; in any case, we ignore them right now, and if there are none we make sure to chop off the final newline, if any. */ cp = strpbrk (args.notifyee, ":\n"); 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); if (line != NULL) free (line); if (args.notifyee == NULL) { char *tmp; tmp = xmalloc (endp - p + 1); strncpy (tmp, p, endp - p); tmp[endp - p] = '\0'; args.notifyee = tmp; } notify_args = &args; args.type = notif; args.who = who; args.file = filename; (void) Parse_Info (CVSROOTADM_NOTIFY, repository, notify_proc, 1); /* It's okay to cast out the const for the free() below since we * just allocated this a few lines above. The const was for * everybody else. */ free ((char *)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 cvs_notify_check (repository, update_dir) const char *repository; const 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] []...\n", "-l\tProcess this directory only (not recursive).\n", "-R\tProcess directories recursively (default).\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; cvs_output (finfo->fullname, 0); p = them; while (1) { cvs_output ("\t", 1); while (*p != '>' && *p != '\0') cvs_output (p++, 1); if (*p == '\0') { /* Only happens if attribute is misformed. */ cvs_output ("\n", 1); break; } ++p; cvs_output ("\t", 1); while (1) { while (*p != '+' && *p != ',' && *p != '\0') cvs_output (p++, 1); if (*p == '\0') { cvs_output ("\n", 1); goto out; } if (*p == ',') { ++p; break; } ++p; cvs_output ("\t", 1); } cvs_output ("\n", 1); } out:; free (them); 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 (current_parsed_root->isremote) { start_server (); ign_setup (); if (local) send_arg ("-l"); send_arg ("--"); send_files (argc, argv, local, 0, SEND_NO_CONTENTS); send_file_names (argc, argv, SEND_EXPAND_WILD); 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, (char *) NULL); }