cvs-cvs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Cvs-cvs] Changes to ccvs/src/client.c [signed-commits2]


From: Derek Robert Price
Subject: [Cvs-cvs] Changes to ccvs/src/client.c [signed-commits2]
Date: Thu, 20 Oct 2005 17:33:57 -0400

Index: ccvs/src/client.c
diff -u /dev/null ccvs/src/client.c:1.433.4.1
--- /dev/null   Thu Oct 20 21:33:56 2005
+++ ccvs/src/client.c   Thu Oct 20 21:33:10 2005
@@ -0,0 +1,5231 @@
+/* CVS client-related stuff.
+
+   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.  */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+#include "cvs.h"
+#include "getline.h"
+#include "edit.h"
+#include "buffer.h"
+#include "save-cwd.h"
+
+#ifdef CLIENT_SUPPORT
+
+# include "log-buffer.h"
+# include "md5.h"
+# include "sign.h"
+
+#include "socket-client.h"
+#include "rsh-client.h"
+
+# ifdef HAVE_GSSAPI
+#   include "gssapi-client.h"
+# endif
+
+# ifdef HAVE_KERBEROS
+#   include "kerberos4-client.h"
+# endif
+
+
+
+/* Keep track of any paths we are sending for Max-dotdot so that we can verify
+ * that uplevel paths coming back form the server are valid.
+ *
+ * FIXME: The correct way to do this is probably provide some sort of virtual
+ * path map on the client side.  This would be generic enough to be applied to
+ * absolute paths supplied by the user too.
+ */
+static List *uppaths;
+
+
+
+static void add_prune_candidate (const char *);
+
+/* All the commands.  */
+int add (int argc, char **argv);
+int admin (int argc, char **argv);
+int checkout (int argc, char **argv);
+int commit (int argc, char **argv);
+int diff (int argc, char **argv);
+int history (int argc, char **argv);
+int import (int argc, char **argv);
+int cvslog (int argc, char **argv);
+int patch (int argc, char **argv);
+int release (int argc, char **argv);
+int cvsremove (int argc, char **argv);
+int rtag (int argc, char **argv);
+int status (int argc, char **argv);
+int tag (int argc, char **argv);
+int update (int argc, char **argv);
+
+static size_t try_read_from_server (char *, size_t);
+
+static void auth_server (cvsroot_t *, struct buffer *, struct buffer *,
+                        int, int, struct hostent *);
+
+
+
+/* This is the referrer who referred us to a primary, or write server, using
+ * the "Redirect" request.
+ */
+static cvsroot_t *client_referrer;
+
+/* We need to keep track of the list of directories we've sent to the
+   server.  This list, along with the current CVSROOT, will help us
+   decide which command-line arguments to send.  */
+List *dirs_sent_to_server;
+static int
+is_arg_a_parent_or_listed_dir (Node *n, void *d)
+{
+    char *directory = n->key;  /* name of the dir sent to server */
+    char *this_argv_elem = d;  /* this argv element */
+
+    /* Say we should send this argument if the argument matches the
+       beginning of a directory name sent to the server.  This way,
+       the server will know to start at the top of that directory
+       hierarchy and descend. */
+
+    if (!strncmp (directory, this_argv_elem, strlen (this_argv_elem)))
+       return 1;
+
+    return 0;
+}
+
+
+
+/* Return nonzero if this argument should not be sent to the
+   server. */
+static int
+arg_should_not_be_sent_to_server (char *arg)
+{
+    /* Decide if we should send this directory name to the server.  We
+       should always send argv[i] if:
+
+       1) the list of directories sent to the server is empty (as it
+       will be for checkout, etc.).
+
+       2) the argument is "."
+
+       3) the argument is a file in the cwd and the cwd is checked out
+       from the current root
+
+       4) the argument lies within one of the paths in
+       dirs_sent_to_server.
+
+       */
+
+    if (list_isempty (dirs_sent_to_server))
+       return 0;               /* always send it */
+
+    if (!strcmp (arg, "."))
+       return 0;               /* always send it */
+
+    /* We should send arg if it is one of the directories sent to the
+       server or the parent of one; this tells the server to descend
+       the hierarchy starting at this level. */
+    if (isdir (arg))
+    {
+       if (walklist (dirs_sent_to_server, is_arg_a_parent_or_listed_dir, arg))
+           return 0;
+
+       /* If arg wasn't a parent, we don't know anything about it (we
+          would have seen something related to it during the
+          send_files phase).  Don't send it.  */
+       return 1;
+    }
+
+    /* Try to decide whether we should send arg to the server by
+       checking the contents of the corresponding CVSADM directory. */
+    {
+       char *t, *root_string;
+       cvsroot_t *this_root = NULL;
+
+       /* Calculate "dirname arg" */
+       for (t = arg + strlen (arg) - 1; t >= arg; t--)
+       {
+           if (ISSLASH (*t))
+               break;
+       }
+
+       /* Now we're either poiting to the beginning of the
+          string, or we found a path separator. */
+       if (t >= arg)
+       {
+           /* Found a path separator.  */
+           char c = *t;
+           *t = '\0';
+           
+           /* First, check to see if we sent this directory to the
+               server, because it takes less time than actually
+               opening the stuff in the CVSADM directory.  */
+           if (walklist (dirs_sent_to_server, is_arg_a_parent_or_listed_dir,
+                         arg))
+           {
+               *t = c;         /* make sure to un-truncate the arg */
+               return 0;
+           }
+
+           /* Since we didn't find it in the list, check the CVSADM
+               files on disk.  */
+           this_root = Name_Root (arg, NULL);
+           root_string = this_root->original;
+           *t = c;
+       }
+       else
+       {
+           /* We're at the beginning of the string.  Look at the
+               CVSADM files in cwd.  */
+           if (CVSroot_cmdline)
+               root_string = CVSroot_cmdline;
+           else
+           {
+               this_root = Name_Root (NULL, NULL);
+               root_string = this_root->original;
+           }
+       }
+
+       /* Now check the value for root. */
+       if (root_string && current_parsed_root
+           && strcmp (root_string, original_parsed_root->original))
+       {
+           /* Don't send this, since the CVSROOTs don't match. */
+           return 1;
+       }
+    }
+    
+    /* OK, let's send it. */
+    return 0;
+}
+#endif /* CLIENT_SUPPORT */
+
+
+
+#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT)
+
+/* Shared with server.  */
+
+/*
+ * Return a malloc'd, '\0'-terminated string
+ * corresponding to the mode in SB.
+ */
+char *
+mode_to_string (mode_t mode)
+{
+    char u[4], g[4], o[4];
+    int i;
+
+    i = 0;
+    if (mode & S_IRUSR) u[i++] = 'r';
+    if (mode & S_IWUSR) u[i++] = 'w';
+    if (mode & S_IXUSR) u[i++] = 'x';
+    u[i] = '\0';
+
+    i = 0;
+    if (mode & S_IRGRP) g[i++] = 'r';
+    if (mode & S_IWGRP) g[i++] = 'w';
+    if (mode & S_IXGRP) g[i++] = 'x';
+    g[i] = '\0';
+
+    i = 0;
+    if (mode & S_IROTH) o[i++] = 'r';
+    if (mode & S_IWOTH) o[i++] = 'w';
+    if (mode & S_IXOTH) o[i++] = 'x';
+    o[i] = '\0';
+
+    return Xasprintf ("u=%s,g=%s,o=%s", u, g, o);
+}
+
+
+
+/*
+ * Change mode of FILENAME to MODE_STRING.
+ * Returns 0 for success or errno code.
+ * If RESPECT_UMASK is set, then honor the umask.
+ */
+int
+change_mode (const char *filename, const char *mode_string, int respect_umask)
+{
+#ifdef CHMOD_BROKEN
+    char *p;
+    int writeable = 0;
+
+    /* We can only distinguish between
+         1) readable
+         2) writeable
+         3) Picasso's "Blue Period"
+       We handle the first two. */
+    p = mode_string;
+    while (*p != '\0')
+    {
+       if ((p[0] == 'u' || p[0] == 'g' || p[0] == 'o') && p[1] == '=')
+       {
+           char *q = p + 2;
+           while (*q != ',' && *q != '\0')
+           {
+               if (*q == 'w')
+                   writeable = 1;
+               ++q;
+           }
+       }
+       /* Skip to the next field.  */
+       while (*p != ',' && *p != '\0')
+           ++p;
+       if (*p == ',')
+           ++p;
+    }
+
+    /* xchmod honors the umask for us.  In the !respect_umask case, we
+       don't try to cope with it (probably to handle that well, the server
+       needs to deal with modes in data structures, rather than via the
+       modes in temporary files).  */
+    xchmod (filename, writeable);
+       return 0;
+
+#else /* ! CHMOD_BROKEN */
+
+    const char *p;
+    mode_t mode = 0;
+    mode_t oumask;
+
+    p = mode_string;
+    while (*p != '\0')
+    {
+       if ((p[0] == 'u' || p[0] == 'g' || p[0] == 'o') && p[1] == '=')
+       {
+           int can_read = 0, can_write = 0, can_execute = 0;
+           const char *q = p + 2;
+           while (*q != ',' && *q != '\0')
+           {
+               if (*q == 'r')
+                   can_read = 1;
+               else if (*q == 'w')
+                   can_write = 1;
+               else if (*q == 'x')
+                   can_execute = 1;
+               ++q;
+           }
+           if (p[0] == 'u')
+           {
+               if (can_read)
+                   mode |= S_IRUSR;
+               if (can_write)
+                   mode |= S_IWUSR;
+               if (can_execute)
+                   mode |= S_IXUSR;
+           }
+           else if (p[0] == 'g')
+           {
+               if (can_read)
+                   mode |= S_IRGRP;
+               if (can_write)
+                   mode |= S_IWGRP;
+               if (can_execute)
+                   mode |= S_IXGRP;
+           }
+           else if (p[0] == 'o')
+           {
+               if (can_read)
+                   mode |= S_IROTH;
+               if (can_write)
+                   mode |= S_IWOTH;
+               if (can_execute)
+                   mode |= S_IXOTH;
+           }
+       }
+       /* Skip to the next field.  */
+       while (*p != ',' && *p != '\0')
+           ++p;
+       if (*p == ',')
+           ++p;
+    }
+
+    if (respect_umask)
+    {
+       oumask = umask (0);
+       (void) umask (oumask);
+       mode &= ~oumask;
+    }
+
+    if (chmod (filename, mode) < 0)
+       return errno;
+    return 0;
+#endif /* ! CHMOD_BROKEN */
+}
+#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */
+
+
+
+#ifdef CLIENT_SUPPORT
+int client_prune_dirs;
+
+static List *ignlist = NULL;
+
+/* Buffer to write to the server.  */
+static struct buffer *global_to_server;
+
+/* Buffer used to read from the server.  */
+static struct buffer *global_from_server;
+
+
+
+/*
+ * Read a line from the server.  Result does not include the terminating \n.
+ *
+ * Space for the result is malloc'd and should be freed by the caller.
+ *
+ * Returns number of bytes read.
+ */
+static size_t
+read_line_via (struct buffer *via_from_buffer, struct buffer *via_to_buffer,
+               char **resultp)
+{
+    int status;
+    char *result;
+    size_t len;
+
+    status = buf_flush (via_to_buffer, 1);
+    if (status != 0)
+       error (1, status, "writing to server");
+
+    status = buf_read_line (via_from_buffer, &result, &len);
+    if (status != 0)
+    {
+       if (status == -1)
+           error (1, 0,
+                   "end of file from server (consult above messages if any)");
+       else if (status == -2)
+           error (1, 0, "out of memory");
+       else
+           error (1, status, "reading from server");
+    }
+
+    if (resultp)
+       *resultp = result;
+    else
+       free (result);
+
+    return len;
+}
+
+
+
+static size_t
+read_line (char **resultp)
+{
+  return read_line_via (global_from_server, global_to_server, resultp);
+}
+#endif /* CLIENT_SUPPORT */
+
+
+
+#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT)
+/*
+ * Zero if compression isn't supported or requested; non-zero to indicate
+ * a compression level to request from gzip.
+ */
+int gzip_level;
+
+/*
+ * Level of compression to use when running gzip on a single file.
+ */
+int file_gzip_level;
+
+#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */
+
+#ifdef CLIENT_SUPPORT
+
+/* Whether the server asked us to force compression.  */
+static bool force_gzip;
+
+/*
+ * The Repository for the top level of this command (not necessarily
+ * the CVSROOT, just the current directory at the time we do it).
+ */
+static char *toplevel_repos;
+
+/* Working directory when we first started.  Note: we could speed things
+   up on some systems by using savecwd.h here instead of just always
+   storing a name.  */
+char *toplevel_wd;
+
+
+
+static void
+handle_ok (char *args, size_t len)
+{
+    return;
+}
+
+
+
+static void
+handle_error (char *args, size_t len)
+{
+    int something_printed;
+    
+    /*
+     * First there is a symbolic error code followed by a space, which
+     * we ignore.
+     */
+    char *p = strchr (args, ' ');
+    if (!p)
+    {
+       error (0, 0, "invalid data from cvs server");
+       return;
+    }
+    ++p;
+
+    /* Next we print the text of the message from the server.  We
+       probably should be prefixing it with "server error" or some
+       such, because if it is something like "Out of memory", the
+       current behavior doesn't say which machine is out of
+       memory.  */
+
+    len -= p - args;
+    something_printed = 0;
+    for (; len > 0; --len)
+    {
+       something_printed = 1;
+       putc (*p++, stderr);
+    }
+    if (something_printed)
+       putc ('\n', stderr);
+}
+
+
+
+static void
+handle_valid_requests (char *args, size_t len)
+{
+    char *p = args;
+    char *q;
+    struct request *rq;
+    do
+    {
+       q = strchr (p, ' ');
+       if (q)
+           *q++ = '\0';
+       for (rq = requests; rq->name; ++rq)
+       {
+           if (!strcmp (rq->name, p))
+               break;
+       }
+       if (!rq->name)
+           /*
+            * It is a request we have never heard of (and thus never
+            * will want to use).  So don't worry about it.
+            */
+           ;
+       else
+       {
+           if (rq->flags & RQ_ENABLEME)
+           {
+               /*
+                * Server wants to know if we have this, to enable the
+                * feature.
+                */
+               send_to_server (rq->name, 0);
+                send_to_server ("\012", 0);
+           }
+           else
+               rq->flags |= RQ_SUPPORTED;
+       }
+       p = q;
+    } while (q);
+    for (rq = requests; rq->name; ++rq)
+    {
+       if ((rq->flags & RQ_SUPPORTED)
+           || (rq->flags & RQ_ENABLEME))
+           continue;
+       if (rq->flags & RQ_ESSENTIAL)
+           error (1, 0, "request `%s' not supported by server", rq->name);
+    }
+}
+
+static void
+handle_force_gzip (char *args, size_t len)
+{
+    force_gzip = true;
+}
+
+
+
+/* Has the server told us its name since the last redirect?
+ */
+static bool referred_since_last_redirect = false;
+static bool free_client_referrer = false;
+
+
+
+static void
+handle_referrer (char *args, size_t len)
+{
+    TRACE (TRACE_FUNCTION, "handle_referrer (%s)", args);
+    client_referrer = parse_cvsroot (args);
+    referred_since_last_redirect = true;
+    free_client_referrer = true;
+}
+
+
+
+/* Redirect our connection to a different server and start over.
+ *
+ * GLOBALS
+ *   current_parsed_root       The CVSROOT being accessed.
+ *   client_referrer           Used to track the server which referred us to a
+ *                             new server.  Can be supplied by the referring
+ *                             server.
+ *   free_client_referrer      Used to track whether the client_referrer needs
+ *                             to be freed before changing it.
+ *   referred_since_last_redirect      
+ *                             Tracks whether the currect server told us how
+ *                             to refer to it.
+ *
+ * OUTPUTS
+ *   current_parsed_root       Updated to point to the new CVSROOT.
+ *   referred_since_last_redirect
+ *                             Always cleared.
+ *   client_referrer           Set automatically to current_parsed_root if
+ *                             the current server did not give us a name to
+ *                             refer to it by.
+ *   free_client_referrer      Reset when necessary.
+ */
+static void
+handle_redirect (char *args, size_t len)
+{
+    static List *redirects = NULL;
+
+    TRACE (TRACE_FUNCTION, "handle_redirect (%s)", args);
+
+    if (redirects && findnode (redirects, args))
+       error (1, 0, "`Redirect' loop detected.  Server misconfiguration?");
+    else
+    {
+       if (!redirects) redirects = getlist();
+       push_string (redirects, args);
+    }
+
+    if (referred_since_last_redirect)
+       referred_since_last_redirect = false;
+    else
+    {
+       if (free_client_referrer) free (client_referrer);
+       client_referrer = current_parsed_root;
+       free_client_referrer = false;
+    }
+
+    current_parsed_root = parse_cvsroot (args);
+
+    /* We deliberately do not set ORIGINAL_PARSED_ROOT here.
+     * ORIGINAL_PARSED_ROOT is used by the client to determine the current root
+     * being processed for the purpose of looking it up in lists and such, even
+     * after a redirect.
+     *
+     * FIXME
+     *   CURRENT_PARSED_ROOT should not be reset by this function.  Redirects
+     *   should be "added" to it.  The REDIRECTS list should also be replaced
+     *   by this new CURRENT_PARSED_ROOT element.  This way, if, for instance,
+     *   a multi-root workspace had two secondaries pointing to the same
+     *   primary, then the client would not report a looping error.
+     *
+     *   There is also a potential memory leak above and storing new roots as
+     *   part of the original could help avoid it fairly elegantly.
+     */
+    if (!current_parsed_root)
+       error (1, 0, "Server requested redirect to invalid root: `%s'",
+              args);
+}
+
+
+
+/*
+ * This is a proc for walklist().  It inverts the error return premise of
+ * walklist.
+ *
+ * RETURNS
+ *   True       If this path is prefixed by one of the paths in walklist and
+ *              does not step above the prefix path.
+ *   False      Otherwise.
+ */
+static
+int path_list_prefixed (Node *p, void *closure)
+{
+    const char *questionable = closure;
+    const char *prefix = p->key;
+    if (strncmp (prefix, questionable, strlen (prefix))) return 0;
+    questionable += strlen (prefix);
+    while (ISSLASH (*questionable)) questionable++;
+    if (*questionable == '\0') return 1;
+    return pathname_levels (questionable);
+}
+
+
+
+/*
+ * Need to validate the client pathname.  Disallowed paths include:
+ *
+ *   1. Absolute paths.
+ *   2. Pathnames that do not reference a specifically requested update
+ *      directory.
+ *
+ * In case 2, we actually only check that the directory is under the uppermost
+ * directories mentioned on the command line.
+ *
+ * RETURNS
+ *   True       If the path is valid.
+ *   False      Otherwise.
+ */
+static
+int is_valid_client_path (const char *pathname)
+{
+    /* 1. Absolute paths. */
+    if (ISABSOLUTE (pathname)) return 0;
+    /* 2. No up-references in path.  */
+    if (pathname_levels (pathname) == 0) return 1;
+    /* 2. No Max-dotdot paths registered.  */
+    if (!uppaths) return 0;
+
+    return walklist (uppaths, path_list_prefixed, (void *)pathname);
+}
+
+
+
+/*
+ * Do all the processing for PATHNAME, where pathname consists of the
+ * repository and the filename.  The parameters we pass to FUNC are:
+ * DATA is just the DATA parameter which was passed to
+ * call_in_directory; ENT_LIST is a pointer to an entries list (which
+ * we manage the storage for); SHORT_PATHNAME is the pathname of the
+ * file relative to the (overall) directory in which the command is
+ * taking place; and FILENAME is the filename portion only of
+ * SHORT_PATHNAME.  When we call FUNC, the curent directory points to
+ * the directory portion of SHORT_PATHNAME.  */
+static void
+call_in_directory (const char *pathname,
+                   void (*func) (void *, List *, const char *, const char *),
+                   void *data)
+{
+    /* This variable holds the result of Entries_Open. */
+    List *last_entries = NULL;
+    char *dir_name;
+    char *filename;
+    /* This is what we get when we hook up the directory (working directory
+       name) from PATHNAME with the filename from REPOSNAME.  For example:
+       pathname: ccvs/src/
+       reposname: /u/src/master/ccvs/foo/ChangeLog
+       short_pathname: ccvs/src/ChangeLog
+       */
+    char *short_pathname;
+    char *p;
+
+    /*
+     * Do the whole descent in parallel for the repositories, so we
+     * know what to put in CVS/Repository files.  I'm not sure the
+     * full hair is necessary since the server does a similar
+     * computation; I suspect that we only end up creating one
+     * directory at a time anyway.
+     *
+     * Also note that we must *only* worry about this stuff when we
+     * are creating directories; `cvs co foo/bar; cd foo/bar; cvs co
+     * CVSROOT; cvs update' is legitimate, but in this case
+     * foo/bar/CVSROOT/CVS/Repository is not a subdirectory of
+     * foo/bar/CVS/Repository.
+     */
+    char *reposname;
+    char *short_repos;
+    char *reposdirname;
+    char *rdirp;
+    int reposdirname_absolute;
+    int newdir = 0;
+
+    assert (pathname);
+
+    reposname = NULL;
+    read_line (&reposname);
+    assert (reposname);
+
+    reposdirname_absolute = 0;
+    if (strncmp (reposname, toplevel_repos, strlen (toplevel_repos)))
+    {
+       reposdirname_absolute = 1;
+       short_repos = reposname;
+    }
+    else
+    {
+       short_repos = reposname + strlen (toplevel_repos) + 1;
+       if (short_repos[-1] != '/')
+       {
+           reposdirname_absolute = 1;
+           short_repos = reposname;
+       }
+    }
+
+   /* Now that we have SHORT_REPOS, we can calculate the path to the file we
+    * are being requested to operate on.
+    */
+    filename = strrchr (short_repos, '/');
+    if (!filename)
+       filename = short_repos;
+    else
+       ++filename;
+
+    short_pathname = xmalloc (strlen (pathname) + strlen (filename) + 5);
+    strcpy (short_pathname, pathname);
+    strcat (short_pathname, filename);
+
+    /* Now that we know the path to the file we were requested to operate on,
+     * we can verify that it is valid.
+     *
+     * For security reasons, if SHORT_PATHNAME is absolute or attempts to
+     * ascend outside of the current sanbbox, we abort.  The server should not
+     * send us anything but relative paths which remain inside the sandbox
+     * here.  Anything less means a trojan CVS server could create and edit
+     * arbitrary files on the client.
+     */
+    if (!is_valid_client_path (short_pathname))
+    {
+       error (0, 0,
+               "Server attempted to update a file via an invalid pathname:");
+        error (1, 0, "`%s'.", short_pathname);
+    }
+
+    reposdirname = xstrdup (short_repos);
+    p = strrchr (reposdirname, '/');
+    if (!p)
+    {
+       reposdirname = xrealloc (reposdirname, 2);
+       reposdirname[0] = '.'; reposdirname[1] = '\0';
+    }
+    else
+       *p = '\0';
+
+    dir_name = xstrdup (pathname);
+    p = strrchr (dir_name, '/');
+    if (!p)
+    {
+       dir_name = xrealloc (dir_name, 2);
+       dir_name[0] = '.'; dir_name[1] = '\0';
+    }
+    else
+       *p = '\0';
+    if (client_prune_dirs)
+       add_prune_candidate (dir_name);
+
+    if (!toplevel_wd)
+    {
+       toplevel_wd = xgetcwd ();
+       if (!toplevel_wd)
+           error (1, errno, "could not get working directory");
+    }
+
+    if (CVS_CHDIR (toplevel_wd) < 0)
+       error (1, errno, "could not chdir to %s", toplevel_wd);
+
+    /* Create the CVS directory at the top level if needed.  The
+       isdir seems like an unneeded system call, but it *does*
+       need to be called both if the CVS_CHDIR below succeeds
+       (e.g.  "cvs co .") or if it fails (e.g. basicb-1a in
+       testsuite).  We only need to do this for the "." case,
+       since the server takes care of forcing this directory to be
+       created in all other cases.  If we don't create CVSADM
+       here, the call to Entries_Open below will fail.  FIXME:
+       perhaps this means that we should change our algorithm
+       below that calls Create_Admin instead of having this code
+       here? */
+    if (/* I think the reposdirname_absolute case has to do with
+          things like "cvs update /foo/bar".  In any event, the
+          code below which tries to put toplevel_repos into
+          CVS/Repository is almost surely unsuited to
+          the reposdirname_absolute case.  */
+       !reposdirname_absolute
+       && !strcmp (dir_name, ".")
+       && ! isdir (CVSADM))
+    {
+       char *repo;
+       char *r;
+
+       newdir = 1;
+
+       /* If toplevel_repos doesn't have at least one character, then the
+        * reference to r[-1] below could be out of bounds.
+        */
+       assert (*toplevel_repos);
+
+       repo = xmalloc (strlen (toplevel_repos)
+                       + 10);
+       strcpy (repo, toplevel_repos);
+       r = repo + strlen (repo);
+       if (r[-1] != '.' || r[-2] != '/')
+           strcpy (r, "/.");
+
+       Create_Admin (".", ".", repo, NULL, NULL, 0, 1, 1);
+
+       free (repo);
+    }
+
+    if (CVS_CHDIR (dir_name) < 0)
+    {
+       char *dir;
+       char *dirp;
+       
+       if (! existence_error (errno))
+           error (1, errno, "could not chdir to %s", dir_name);
+       
+       /* Directory does not exist, we need to create it.  */
+       newdir = 1;
+
+       /* Provided we are willing to assume that directories get
+          created one at a time, we could simplify this a lot.
+          Do note that one aspect still would need to walk the
+          dir_name path: the checking for "fncmp (dir, CVSADM)".  */
+
+       dir = xmalloc (strlen (dir_name) + 1);
+       dirp = dir_name;
+       rdirp = reposdirname;
+
+       /* This algorithm makes nested directories one at a time
+          and create CVS administration files in them.  For
+          example, we're checking out foo/bar/baz from the
+          repository:
+
+          1) create foo, point CVS/Repository to <root>/foo
+          2)     .. foo/bar                   .. <root>/foo/bar
+          3)     .. foo/bar/baz               .. <root>/foo/bar/baz
+          
+          As you can see, we're just stepping along DIR_NAME (with
+          DIRP) and REPOSDIRNAME (with RDIRP) respectively.
+
+          We need to be careful when we are checking out a
+          module, however, since DIR_NAME and REPOSDIRNAME are not
+          going to be the same.  Since modules will not have any
+          slashes in their names, we should watch the output of
+          STRCHR to decide whether or not we should use STRCHR on
+          the RDIRP.  That is, if we're down to a module name,
+          don't keep picking apart the repository directory name.  */
+
+       do
+       {
+           dirp = strchr (dirp, '/');
+           if (dirp)
+           {
+               strncpy (dir, dir_name, dirp - dir_name);
+               dir[dirp - dir_name] = '\0';
+               /* Skip the slash.  */
+               ++dirp;
+               if (!rdirp)
+                   /* This just means that the repository string has
+                      fewer components than the dir_name string.  But
+                      that is OK (e.g. see modules3-8 in testsuite).  */
+                   ;
+               else
+                   rdirp = strchr (rdirp, '/');
+           }
+           else
+           {
+               /* If there are no more slashes in the dir name,
+                  we're down to the most nested directory -OR- to
+                  the name of a module.  In the first case, we
+                  should be down to a DIRP that has no slashes,
+                  so it won't help/hurt to do another STRCHR call
+                  on DIRP.  It will definitely hurt, however, if
+                  we're down to a module name, since a module
+                  name can point to a nested directory (that is,
+                  DIRP will still have slashes in it.  Therefore,
+                  we should set it to NULL so the routine below
+                  copies the contents of REMOTEDIRNAME onto the
+                  root repository directory (does this if rdirp
+                  is set to NULL, because we used to do an extra
+                  STRCHR call here). */
+
+               rdirp = NULL;
+               strcpy (dir, dir_name);
+           }
+
+           if (fncmp (dir, CVSADM) == 0)
+           {
+               error (0, 0, "cannot create a directory named %s", dir);
+               error (0, 0, "because CVS uses \"%s\" for its own uses",
+                      CVSADM);
+               error (1, 0, "rename the directory and try again");
+           }
+
+           if (mkdir_if_needed (dir))
+           {
+               /* It already existed, fine.  Just keep going.  */
+           }
+           else if (!strcmp (cvs_cmd_name, "export"))
+               /* Don't create CVSADM directories if this is export.  */
+               ;
+           else
+           {
+               /*
+                * Put repository in CVS/Repository.  For historical
+                * (pre-CVS/Root) reasons, this is an absolute pathname,
+                * but what really matters is the part of it which is
+                * relative to cvsroot.
+                */
+               char *repo;
+               char *r, *b;
+
+               repo = xmalloc (strlen (reposdirname)
+                               + strlen (toplevel_repos)
+                               + 80);
+               if (reposdirname_absolute)
+                   r = repo;
+               else
+               {
+                   strcpy (repo, toplevel_repos);
+                   strcat (repo, "/");
+                   r = repo + strlen (repo);
+               }
+
+               if (rdirp)
+               {
+                   /* See comment near start of function; the only
+                      way that the server can put the right thing
+                      in each CVS/Repository file is to create the
+                      directories one at a time.  I think that the
+                      CVS server has been doing this all along.  */
+                   error (0, 0, "\
+warning: server is not creating directories one at a time");
+                   strncpy (r, reposdirname, rdirp - reposdirname);
+                   r[rdirp - reposdirname] = '\0';
+               }
+               else
+                   strcpy (r, reposdirname);
+
+               Create_Admin (dir, dir, repo, NULL, NULL, 0, 0, 1);
+               free (repo);
+
+               b = strrchr (dir, '/');
+               if (!b)
+                   Subdir_Register (NULL, NULL, dir);
+               else
+               {
+                   *b = '\0';
+                   Subdir_Register (NULL, dir, b + 1);
+                   *b = '/';
+               }
+           }
+
+           if (rdirp)
+           {
+               /* Skip the slash.  */
+               ++rdirp;
+           }
+
+       } while (dirp);
+       free (dir);
+       /* Now it better work.  */
+       if (CVS_CHDIR (dir_name) < 0)
+           error (1, errno, "could not chdir to %s", dir_name);
+    }
+    else if (!strcmp (cvs_cmd_name, "export"))
+       /* Don't create CVSADM directories if this is export.  */
+       ;
+    else if (!isdir (CVSADM))
+    {
+       /*
+        * Put repository in CVS/Repository.  For historical
+        * (pre-CVS/Root) reasons, this is an absolute pathname,
+        * but what really matters is the part of it which is
+        * relative to cvsroot.
+        */
+       char *repo;
+
+       if (reposdirname_absolute)
+           repo = reposdirname;
+       else
+           repo = Xasprintf ("%s/%s", toplevel_repos, reposdirname);
+
+       Create_Admin (".", ".", repo, NULL, NULL, 0, 1, 1);
+       if (repo != reposdirname)
+           free (repo);
+    }
+
+    if (strcmp (cvs_cmd_name, "export"))
+    {
+       last_entries = Entries_Open (0, dir_name);
+
+       /* If this is a newly created directory, we will record
+          all subdirectory information, so call Subdirs_Known in
+          case there are no subdirectories.  If this is not a
+          newly created directory, it may be an old working
+          directory from before we recorded subdirectory
+          information in the Entries file.  We force a search for
+          all subdirectories now, to make sure our subdirectory
+          information is up to date.  If the Entries file does
+          record subdirectory information, then this call only
+          does list manipulation.  */
+       if (newdir)
+           Subdirs_Known (last_entries);
+       else
+       {
+           List *dirlist;
+
+           dirlist = Find_Directories (NULL, W_LOCAL, last_entries);
+           dellist (&dirlist);
+       }
+    }
+    free (reposdirname);
+    (*func) (data, last_entries, short_pathname, filename);
+    if (last_entries)
+       Entries_Close (last_entries);
+    free (dir_name);
+    free (short_pathname);
+    free (reposname);
+}
+
+
+
+static void
+copy_a_file (void *data, List *ent_list, const char *short_pathname,
+            const char *filename)
+{
+    char *newname;
+
+    read_line (&newname);
+
+#ifdef USE_VMS_FILENAMES
+    {
+       /* Mogrify the filename so VMS is happy with it. */
+       char *p;
+       for(p = newname; *p; p++)
+          if(*p == '.' || *p == '#') *p = '_';
+    }
+#endif
+    /* cvsclient.texi has said for a long time that newname must be in the
+       same directory.  Wouldn't want a malicious or buggy server overwriting
+       ~/.profile, /etc/passwd, or anything like that.  */
+    if (last_component (newname) != newname)
+       error (1, 0, "protocol error: Copy-file tried to specify directory");
+
+    if (unlink_file (newname) && !existence_error (errno))
+       error (0, errno, "unable to remove %s", newname);
+    copy_file (filename, newname);
+    free (newname);
+}
+
+
+
+static void
+handle_copy_file (char *args, size_t len)
+{
+    call_in_directory (args, copy_a_file, NULL);
+}
+
+
+
+/* Read from the server the count for the length of a file, then read
+   the contents of that file and write them to FILENAME.  FULLNAME is
+   the name of the file for use in error messages.  FIXME-someday:
+   extend this to deal with compressed files and make update_entries
+   use it.  On error, gives a fatal error.  */
+static void
+read_counted_file (char *filename, char *fullname)
+{
+    char *size_string;
+    size_t size;
+    char *buf;
+
+    /* Pointers in buf to the place to put data which will be read,
+       and the data which needs to be written, respectively.  */
+    char *pread;
+    char *pwrite;
+    /* Number of bytes left to read and number of bytes in buf waiting to
+       be written, respectively.  */
+    size_t nread;
+    size_t nwrite;
+
+    FILE *fp;
+
+    read_line (&size_string);
+    if (size_string[0] == 'z')
+       error (1, 0, "\
+protocol error: compressed files not supported for that operation");
+    /* FIXME: should be doing more error checking, probably.  Like using
+       strtoul and making sure we used up the whole line.  */
+    size = atoi (size_string);
+    free (size_string);
+
+    /* A more sophisticated implementation would use only a limited amount
+       of buffer space (8K perhaps), and read that much at a time.  We allocate
+       a buffer for the whole file only to make it easy to keep track what
+       needs to be read and written.  */
+    buf = xmalloc (size);
+
+    /* FIXME-someday: caller should pass in a flag saying whether it
+       is binary or not.  I haven't carefully looked into whether
+       CVS/Template files should use local text file conventions or
+       not.  */
+    fp = CVS_FOPEN (filename, "wb");
+    if (!fp)
+       error (1, errno, "cannot write %s", fullname);
+    nread = size;
+    nwrite = 0;
+    pread = buf;
+    pwrite = buf;
+    while (nread > 0 || nwrite > 0)
+    {
+       size_t n;
+
+       if (nread > 0)
+       {
+           n = try_read_from_server (pread, nread);
+           nread -= n;
+           pread += n;
+           nwrite += n;
+       }
+
+       if (nwrite > 0)
+       {
+           n = fwrite (pwrite, sizeof *pwrite, nwrite, fp);
+           if (ferror (fp))
+               error (1, errno, "cannot write %s", fullname);
+           nwrite -= n;
+           pwrite += n;
+       }
+    }
+    free (buf);
+    if (fclose (fp) < 0)
+       error (1, errno, "cannot close %s", fullname);
+}
+
+
+
+/* OK, we want to swallow the "U foo.c" response and then output it only
+   if we can update the file.  In the future we probably want some more
+   systematic approach to parsing tagged text, but for now we keep it
+   ad hoc.  "Why," I hear you cry, "do we not just look at the
+   Update-existing and Created responses?"  That is an excellent question,
+   and the answer is roughly conservatism/laziness--I haven't read through
+   update.c enough to figure out the exact correspondence or lack thereof
+   between those responses and a "U foo.c" line (note that Merged, from
+   join_file, can be either "C foo" or "U foo" depending on the context).  */
+/* Nonzero if we have seen +updated and not -updated.  */
+static int updated_seen;
+/* Filename from an "fname" tagged response within +updated/-updated.  */
+static char *updated_fname;
+
+/* This struct is used to hold data when reading the +importmergecmd
+   and -importmergecmd tags.  We put the variables in a struct only
+   for namespace issues.  FIXME: As noted above, we need to develop a
+   more systematic approach.  */
+static struct
+{
+    /* Nonzero if we have seen +importmergecmd and not -importmergecmd.  */
+    int seen;
+    /* Number of conflicts, from a "conflicts" tagged response.  */
+    int conflicts;
+    /* First merge tag, from a "mergetag1" tagged response.  */
+    char *mergetag1;
+    /* Second merge tag, from a "mergetag2" tagged response.  */
+    char *mergetag2;
+    /* Repository, from a "repository" tagged response.  */
+    char *repository;
+} importmergecmd;
+
+/* Nonzero if we should arrange to return with a failure exit status.  */
+static bool failure_exit;
+
+
+/*
+ * The time stamp of the last file we registered.
+ */
+static time_t last_register_time;
+
+
+
+/*
+ * The Checksum response gives the checksum for the file transferred
+ * over by the next Updated, Merged or Patch response.  We just store
+ * it here, and then check it in update_entries.
+ */
+static int stored_checksum_valid;
+static unsigned char stored_checksum[16];
+static void
+handle_checksum (char *args, size_t len)
+{
+    char *s;
+    char buf[3];
+    int i;
+
+    if (stored_checksum_valid)
+        error (1, 0, "Checksum received before last one was used");
+
+    s = args;
+    buf[2] = '\0';
+    for (i = 0; i < 16; i++)
+    {
+        char *bufend;
+
+       buf[0] = *s++;
+       buf[1] = *s++;
+       stored_checksum[i] = (char) strtol (buf, &bufend, 16);
+       if (bufend != buf + 2)
+           break;
+    }
+
+    if (i < 16 || *s != '\0')
+        error (1, 0, "Invalid Checksum response: `%s'", args);
+
+    stored_checksum_valid = 1;
+}
+
+
+
+/* Mode that we got in a "Mode" response (malloc'd), or NULL if none.  */
+static char *stored_mode;
+static void
+handle_mode (char *args, size_t len)
+{
+    if (stored_mode)
+       error (1, 0, "protocol error: duplicate Mode");
+    stored_mode = xstrdup (args);
+}
+
+
+
+/* Nonzero if time was specified in Mod-time.  */
+static int stored_modtime_valid;
+/* Time specified in Mod-time.  */
+static time_t stored_modtime;
+static void
+handle_mod_time (char *args, size_t len)
+{
+    struct timespec newtime;
+    if (stored_modtime_valid)
+       error (0, 0, "protocol error: duplicate Mod-time");
+    if (get_date (&newtime, args, NULL))
+    {
+       /* Truncate nanoseconds.  */
+       stored_modtime = newtime.tv_sec;
+       stored_modtime_valid = 1;
+    }
+    else
+       error (0, 0, "protocol error: cannot parse date %s", args);
+}
+
+
+
+/*
+ * If we receive a patch, but the patch program fails to apply it, we
+ * want to request the original file.  We keep a list of files whose
+ * patches have failed.
+ */
+
+char **failed_patches;
+int failed_patches_count;
+
+struct update_entries_data
+{
+    enum {
+      /*
+       * We are just getting an Entries line; the local file is
+       * correct.
+       */
+      UPDATE_ENTRIES_CHECKIN,
+      /* We are getting the file contents as well.  */
+      UPDATE_ENTRIES_UPDATE,
+      /*
+       * We are getting a patch against the existing local file, not
+       * an entire new file.
+       */
+      UPDATE_ENTRIES_PATCH,
+      /*
+       * We are getting an RCS change text (diff -n output) against
+       * the existing local file, not an entire new file.
+       */
+      UPDATE_ENTRIES_RCS_DIFF
+    } contents;
+
+    enum {
+       /* We are replacing an existing file.  */
+       UPDATE_ENTRIES_EXISTING,
+       /* We are creating a new file.  */
+       UPDATE_ENTRIES_NEW,
+       /* We don't know whether it is existing or new.  */
+       UPDATE_ENTRIES_EXISTING_OR_NEW
+    } existp;
+
+    /*
+     * String to put in the timestamp field or NULL to use the timestamp
+     * of the file.
+     */
+    char *timestamp;
+};
+
+
+
+/* Update the Entries line for this file.  */
+static void
+update_entries (void *data_arg, List *ent_list, const char *short_pathname,
+                const char *filename)
+{
+    char *entries_line;
+    struct update_entries_data *data = data_arg;
+
+    char *cp;
+    char *user;
+    char *vn;
+    /* Timestamp field.  Always empty according to the protocol.  */
+    char *ts;
+    char *options = NULL;
+    char *tag = NULL;
+    char *date = NULL;
+    char *tag_or_date;
+    char *scratch_entries = NULL;
+    int bin;
+
+#ifdef UTIME_EXPECTS_WRITABLE
+    int change_it_back = 0;
+#endif
+
+    read_line (&entries_line);
+
+    /*
+     * Parse the entries line.
+     */
+    scratch_entries = xstrdup (entries_line);
+
+    if (scratch_entries[0] != '/')
+        error (1, 0, "bad entries line `%s' from server", entries_line);
+    user = scratch_entries + 1;
+    if (!(cp = strchr (user, '/')))
+        error (1, 0, "bad entries line `%s' from server", entries_line);
+    *cp++ = '\0';
+    vn = cp;
+    if (!(cp = strchr (vn, '/')))
+        error (1, 0, "bad entries line `%s' from server", entries_line);
+    *cp++ = '\0';
+    
+    ts = cp;
+    if (!(cp = strchr (ts, '/')))
+        error (1, 0, "bad entries line `%s' from server", entries_line);
+    *cp++ = '\0';
+    options = cp;
+    if (!(cp = strchr (options, '/')))
+        error (1, 0, "bad entries line `%s' from server", entries_line);
+    *cp++ = '\0';
+    tag_or_date = cp;
+    
+    /* If a slash ends the tag_or_date, ignore everything after it.  */
+    cp = strchr (tag_or_date, '/');
+    if (cp)
+        *cp = '\0';
+    if (*tag_or_date == 'T')
+        tag = tag_or_date + 1;
+    else if (*tag_or_date == 'D')
+        date = tag_or_date + 1;
+
+    /* Done parsing the entries line. */
+
+    if (data->contents == UPDATE_ENTRIES_UPDATE
+       || data->contents == UPDATE_ENTRIES_PATCH
+       || data->contents == UPDATE_ENTRIES_RCS_DIFF)
+    {
+       char *size_string;
+       char *mode_string;
+       int size;
+       char *buf;
+       char *temp_filename;
+       int use_gzip;
+       int patch_failed;
+
+       read_line (&mode_string);
+       
+       read_line (&size_string);
+       if (size_string[0] == 'z')
+       {
+           use_gzip = 1;
+           size = atoi (size_string+1);
+       }
+       else
+       {
+           use_gzip = 0;
+           size = atoi (size_string);
+       }
+       free (size_string);
+
+       /* Note that checking this separately from writing the file is
+          a race condition: if the existence or lack thereof of the
+          file changes between now and the actual calls which
+          operate on it, we lose.  However (a) there are so many
+          cases, I'm reluctant to try to fix them all, (b) in some
+          cases the system might not even have a system call which
+          does the right thing, and (c) it isn't clear this needs to
+          work.  */
+       if (data->existp == UPDATE_ENTRIES_EXISTING
+           && !isfile (filename))
+           /* Emit a warning and update the file anyway.  */
+           error (0, 0, "warning: %s unexpectedly disappeared",
+                  short_pathname);
+
+       if (data->existp == UPDATE_ENTRIES_NEW
+           && isfile (filename))
+       {
+           /* Emit a warning and refuse to update the file; we don't want
+              to clobber a user's file.  */
+           size_t nread;
+           size_t toread;
+
+           /* size should be unsigned, but until we get around to fixing
+              that, work around it.  */
+           size_t usize;
+
+           char buf[8192];
+
+           /* This error might be confusing; it isn't really clear to
+              the user what to do about it.  Keep in mind that it has
+              several causes: (1) something/someone creates the file
+              during the time that CVS is running, (2) the repository
+              has two files whose names clash for the client because
+              of case-insensitivity or similar causes, See 3 for
+              additional notes.  (3) a special case of this is that a
+              file gets renamed for example from a.c to A.C.  A
+              "cvs update" on a case-insensitive client will get this
+              error.  In this case and in case 2, the filename
+              (short_pathname) printed in the error message will likely _not_
+              have the same case as seen by the user in a directory listing.
+              (4) the client has a file which the server doesn't know
+              about (e.g. "? foo" file), and that name clashes with a file
+              the server does know about, (5) classify.c will print the same
+              message for other reasons.
+
+              I hope the above paragraph makes it clear that making this
+              clearer is not a one-line fix.  */
+           error (0, 0, "move away `%s'; it is in the way", short_pathname);
+           if (updated_fname)
+           {
+               cvs_output ("C ", 0);
+               cvs_output (updated_fname, 0);
+               cvs_output ("\n", 1);
+           }
+           failure_exit = true;
+
+       discard_file_and_return:
+           /* Now read and discard the file contents.  */
+           usize = size;
+           nread = 0;
+           while (nread < usize)
+           {
+               toread = usize - nread;
+               if (toread > sizeof buf)
+                   toread = sizeof buf;
+
+               nread += try_read_from_server (buf, toread);
+               if (nread == usize)
+                   break;
+           }
+
+           free (mode_string);
+           free (scratch_entries);
+           free (entries_line);
+
+           /* The Mode, Mod-time, and Checksum responses should not carry
+              over to a subsequent Created (or whatever) response, even
+              in the error case.  */
+           if (stored_mode)
+           {
+               free (stored_mode);
+               stored_mode = NULL;
+           }
+           stored_modtime_valid = 0;
+           stored_checksum_valid = 0;
+
+           if (updated_fname)
+           {
+               free (updated_fname);
+               updated_fname = NULL;
+           }
+           return;
+       }
+
+       temp_filename = xmalloc (strlen (filename) + 80);
+#ifdef USE_VMS_FILENAMES
+        /* A VMS rename of "blah.dat" to "foo" to implies a
+           destination of "foo.dat" which is unfortinate for CVS */
+       sprintf (temp_filename, "%s_new_", filename);
+#else
+#ifdef _POSIX_NO_TRUNC
+       sprintf (temp_filename, ".new.%.9s", filename);
+#else /* _POSIX_NO_TRUNC */
+       sprintf (temp_filename, ".new.%s", filename);
+#endif /* _POSIX_NO_TRUNC */
+#endif /* USE_VMS_FILENAMES */
+
+       buf = xmalloc (size);
+
+        /* Some systems, like OS/2 and Windows NT, end lines with CRLF
+           instead of just LF.  Format translation is done in the C
+           library I/O funtions.  Here we tell them whether or not to
+           convert -- if this file is marked "binary" with the RCS -kb
+           flag, then we don't want to convert, else we do (because
+           CVS assumes text files by default). */
+
+       if (options)
+           bin = !strcmp (options, "-kb");
+       else
+           bin = 0;
+
+       if (data->contents == UPDATE_ENTRIES_RCS_DIFF)
+       {
+           /* This is an RCS change text.  We just hold the change
+              text in memory.  */
+
+           if (use_gzip)
+               error (1, 0,
+                      "server error: gzip invalid with RCS change text");
+
+           read_from_server (buf, size);
+       }
+       else
+       {
+           int fd;
+
+           fd = CVS_OPEN (temp_filename,
+                          (O_WRONLY | O_CREAT | O_TRUNC
+                           | (bin ? OPEN_BINARY : 0)),
+                          0777);
+
+           if (fd < 0)
+           {
+               /* I can see a case for making this a fatal error; for
+                  a condition like disk full or network unreachable
+                  (for a file server), carrying on and giving an
+                  error on each file seems unnecessary.  But if it is
+                  a permission problem, or some such, then it is
+                  entirely possible that future files will not have
+                  the same problem.  */
+               error (0, errno, "cannot write %s", short_pathname);
+               free (temp_filename);
+               free (buf);
+               goto discard_file_and_return;
+           }
+
+           if (size > 0)
+           {
+               read_from_server (buf, size);
+
+               if (use_gzip)
+               {
+                   if (gunzip_and_write (fd, short_pathname, 
+                                         (unsigned char *) buf, size))
+                       error (1, 0, "aborting due to compression error");
+               }
+               else if (write (fd, buf, size) != size)
+                   error (1, errno, "writing %s", short_pathname);
+           }
+
+           if (close (fd) < 0)
+               error (1, errno, "writing %s", short_pathname);
+       }
+
+       /* This is after we have read the file from the net (a change
+          from previous versions, where the server would send us
+          "M U foo.c" before Update-existing or whatever), but before
+          we finish writing the file (arguably a bug).  The timing
+          affects a user who wants status info about how far we have
+          gotten, and also affects whether "U foo.c" appears in addition
+          to various error messages.  */
+       if (updated_fname)
+       {
+           cvs_output ("U ", 0);
+           cvs_output (updated_fname, 0);
+           cvs_output ("\n", 1);
+           free (updated_fname);
+           updated_fname = 0;
+       }
+
+       patch_failed = 0;
+
+       if (data->contents == UPDATE_ENTRIES_UPDATE)
+       {
+           rename_file (temp_filename, filename);
+       }
+       else if (data->contents == UPDATE_ENTRIES_PATCH)
+       {
+           /* You might think we could just leave Patched out of
+              Valid-responses and not get this response.  However, if
+              memory serves, the CVS 1.9 server bases this on -u
+              (update-patches), and there is no way for us to send -u
+              or not based on whether the server supports "Rcs-diff".  
+
+              Fall back to transmitting entire files.  */
+           patch_failed = 1;
+       }
+       else
+       {
+           char *filebuf;
+           size_t filebufsize;
+           size_t nread;
+           char *patchedbuf;
+           size_t patchedlen;
+
+           /* Handle UPDATE_ENTRIES_RCS_DIFF.  */
+
+           if (!isfile (filename))
+               error (1, 0, "patch original file %s does not exist",
+                      short_pathname);
+           filebuf = NULL;
+           filebufsize = 0;
+           nread = 0;
+
+           get_file (filename, short_pathname, bin ? FOPEN_BINARY_READ : "r",
+                     &filebuf, &filebufsize, &nread);
+           /* At this point the contents of the existing file are in
+               FILEBUF, and the length of the contents is in NREAD.
+               The contents of the patch from the network are in BUF,
+               and the length of the patch is in SIZE.  */
+
+           if (! rcs_change_text (short_pathname, filebuf, nread, buf, size,
+                                  &patchedbuf, &patchedlen))
+               patch_failed = 1;
+           else
+           {
+               if (stored_checksum_valid)
+               {
+                   unsigned char checksum[16];
+
+                   /* We have a checksum.  Check it before writing
+                      the file out, so that we don't have to read it
+                      back in again.  */
+                   md5_buffer (patchedbuf, patchedlen, checksum);
+                   if (memcmp (checksum, stored_checksum, 16) != 0)
+                   {
+                       error (0, 0,
+"checksum failure after patch to %s; will refetch",
+                              short_pathname);
+
+                       patch_failed = 1;
+                   }
+
+                   stored_checksum_valid = 0;
+               }
+
+               if (! patch_failed)
+               {
+                   FILE *e;
+
+                   e = xfopen (temp_filename,
+                               bin ? FOPEN_BINARY_WRITE : "w");
+                   if (fwrite (patchedbuf, sizeof *patchedbuf, patchedlen, e)
+                       != patchedlen)
+                       error (1, errno, "cannot write %s", temp_filename);
+                   if (fclose (e) == EOF)
+                       error (1, errno, "cannot close %s", temp_filename);
+                   rename_file (temp_filename, filename);
+               }
+
+               free (patchedbuf);
+           }
+
+           free (filebuf);
+       }
+
+       free (temp_filename);
+
+       if (stored_checksum_valid && ! patch_failed)
+       {
+           FILE *e;
+           struct md5_ctx context;
+           unsigned char buf[8192];
+           unsigned len;
+           unsigned char checksum[16];
+
+           /*
+            * Compute the MD5 checksum.  This will normally only be
+            * used when receiving a patch, so we always compute it
+            * here on the final file, rather than on the received
+            * data.
+            *
+            * Note that if the file is a text file, we should read it
+            * here using text mode, so its lines will be terminated the same
+            * way they were transmitted.
+            */
+           e = CVS_FOPEN (filename, "r");
+           if (!e)
+               error (1, errno, "could not open %s", short_pathname);
+
+           md5_init_ctx (&context);
+           while ((len = fread (buf, 1, sizeof buf, e)) != 0)
+               md5_process_bytes (buf, len, &context);
+           if (ferror (e))
+               error (1, errno, "could not read %s", short_pathname);
+           md5_finish_ctx (&context, checksum);
+
+           fclose (e);
+
+           stored_checksum_valid = 0;
+
+           if (memcmp (checksum, stored_checksum, 16) != 0)
+           {
+               if (data->contents != UPDATE_ENTRIES_PATCH)
+                   error (1, 0, "checksum failure on %s",
+                          short_pathname);
+
+               error (0, 0,
+                      "checksum failure after patch to %s; will refetch",
+                      short_pathname);
+
+               patch_failed = 1;
+           }
+       }
+
+       if (patch_failed)
+       {
+           /* Save this file to retrieve later.  */
+           failed_patches = xnrealloc (failed_patches,
+                                       failed_patches_count + 1,
+                                       sizeof (char *));
+           failed_patches[failed_patches_count] = xstrdup (short_pathname);
+           ++failed_patches_count;
+
+           stored_checksum_valid = 0;
+
+           free (mode_string);
+           free (buf);
+           free (scratch_entries);
+           free (entries_line);
+
+           return;
+       }
+
+        {
+           int status = change_mode (filename, mode_string, 1);
+           if (status != 0)
+               error (0, status, "cannot change mode of %s", short_pathname);
+       }
+
+       free (mode_string);
+       free (buf);
+    }
+
+    if (stored_mode)
+    {
+       change_mode (filename, stored_mode, 1);
+       free (stored_mode);
+       stored_mode = NULL;
+    }
+   
+    if (stored_modtime_valid)
+    {
+       struct utimbuf t;
+
+       memset (&t, 0, sizeof (t));
+       t.modtime = stored_modtime;
+       (void) time (&t.actime);
+
+#ifdef UTIME_EXPECTS_WRITABLE
+       if (!iswritable (filename))
+       {
+           xchmod (filename, 1);
+           change_it_back = 1;
+       }
+#endif  /* UTIME_EXPECTS_WRITABLE  */
+
+       if (utime (filename, &t) < 0)
+           error (0, errno, "cannot set time on %s", filename);
+
+#ifdef UTIME_EXPECTS_WRITABLE
+       if (change_it_back)
+       {
+           xchmod (filename, 0);
+           change_it_back = 0;
+       }
+#endif  /*  UTIME_EXPECTS_WRITABLE  */
+
+       stored_modtime_valid = 0;
+    }
+
+    /*
+     * Process the entries line.  Do this after we've written the file,
+     * since we need the timestamp.
+     */
+    if (strcmp (cvs_cmd_name, "export"))
+    {
+       char *local_timestamp;
+       char *file_timestamp;
+
+       (void) time (&last_register_time);
+
+       local_timestamp = data->timestamp;
+       if (!local_timestamp || ts[0] == '+')
+           file_timestamp = time_stamp (filename);
+       else
+           file_timestamp = NULL;
+
+       /*
+        * These special version numbers signify that it is not up to
+        * date.  Create a dummy timestamp which will never compare
+        * equal to the timestamp of the file.
+        */
+       if (vn[0] == '\0' || !strcmp (vn, "0") || vn[0] == '-')
+           local_timestamp = "dummy timestamp";
+       else if (!local_timestamp)
+       {
+           local_timestamp = file_timestamp;
+
+           /* Checking for cvs_cmd_name of "commit" doesn't seem like
+              the cleanest way to handle this, but it seem to roughly
+              parallel what the :local: code which calls
+              mark_up_to_date ends up amounting to.  Some day, should
+              think more about what the Checked-in response means
+              vis-a-vis both Entries and Base and clarify
+              cvsclient.texi accordingly.  */
+
+           if (!strcmp (cvs_cmd_name, "commit"))
+               mark_up_to_date (filename);
+       }
+
+       Register (ent_list, filename, vn, local_timestamp,
+                 options, tag, date, ts[0] == '+' ? file_timestamp : NULL);
+
+       if (file_timestamp)
+           free (file_timestamp);
+
+    }
+    free (scratch_entries);
+    free (entries_line);
+}
+
+
+
+static void
+handle_checked_in (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_CHECKIN;
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_new_entry (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_CHECKIN;
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = "dummy timestamp from new-entry";
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_updated (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_UPDATE;
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_created (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_UPDATE;
+    dat.existp = UPDATE_ENTRIES_NEW;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_update_existing (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_UPDATE;
+    dat.existp = UPDATE_ENTRIES_EXISTING;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_merged (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_UPDATE;
+    /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case...  */
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = "Result of merge";
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_patched (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_PATCH;
+    /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case...  */
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+handle_rcs_diff (char *args, size_t len)
+{
+    struct update_entries_data dat;
+    dat.contents = UPDATE_ENTRIES_RCS_DIFF;
+    /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case...  */
+    dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW;
+    dat.timestamp = NULL;
+    call_in_directory (args, update_entries, &dat);
+}
+
+
+
+static void
+remove_entry (void *data, List *ent_list, const char *short_pathname,
+              const char *filename)
+{
+    Scratch_Entry (ent_list, filename);
+}
+
+
+
+static void
+handle_remove_entry (char *args, size_t len)
+{
+    call_in_directory (args, remove_entry, NULL);
+}
+
+
+
+static void
+remove_entry_and_file (void *data, List *ent_list, const char *short_pathname,
+                       const char *filename)
+{
+    Scratch_Entry (ent_list, filename);
+    /* Note that we don't ignore existence_error's here.  The server
+       should be sending Remove-entry rather than Removed in cases
+       where the file does not exist.  And if the user removes the
+       file halfway through a cvs command, we should be printing an
+       error.  */
+    if (unlink_file (filename) < 0)
+       error (0, errno, "unable to remove %s", short_pathname);
+}
+
+
+
+static void
+handle_removed (char *args, size_t len)
+{
+    call_in_directory (args, remove_entry_and_file, NULL);
+}
+
+
+
+/* Is this the top level (directory containing CVSROOT)?  */
+static int
+is_cvsroot_level (char *pathname)
+{
+    if (strcmp (toplevel_repos, current_parsed_root->directory))
+       return 0;
+
+    return !strchr (pathname, '/');
+}
+
+
+
+static void
+set_static (void *data, List *ent_list, const char *short_pathname,
+           const char *filename)
+{
+    FILE *fp;
+    fp = xfopen (CVSADM_ENTSTAT, "w+");
+    if (fclose (fp) == EOF)
+        error (1, errno, "cannot close %s", CVSADM_ENTSTAT);
+}
+
+
+
+static void
+handle_set_static_directory (char *args, size_t len)
+{
+    if (!strcmp (cvs_cmd_name, "export"))
+    {
+       /* Swallow the repository.  */
+       read_line (NULL);
+       return;
+    }
+    call_in_directory (args, set_static, NULL);
+}
+
+
+
+static void
+clear_static (void *data, List *ent_list, const char *short_pathname,
+              const char *filename)
+{
+    if (unlink_file (CVSADM_ENTSTAT) < 0 && ! existence_error (errno))
+        error (1, errno, "cannot remove file %s", CVSADM_ENTSTAT);
+}
+
+
+
+static void
+handle_clear_static_directory (char *pathname, size_t len)
+{
+    if (!strcmp (cvs_cmd_name, "export"))
+    {
+       /* Swallow the repository.  */
+       read_line (NULL);
+       return;
+    }
+
+    if (is_cvsroot_level (pathname))
+    {
+        /*
+        * Top level (directory containing CVSROOT).  This seems to normally
+        * lack a CVS directory, so don't try to create files in it.
+        */
+       return;
+    }
+    call_in_directory (pathname, clear_static, NULL);
+}
+
+
+
+static void
+set_sticky (void *data, List *ent_list, const char *short_pathname,
+           const char *filename)
+{
+    char *tagspec;
+    FILE *f;
+
+    read_line (&tagspec);
+
+    /* FIXME-update-dir: error messages should include the directory.  */
+    f = CVS_FOPEN (CVSADM_TAG, "w+");
+    if (!f)
+    {
+       /* Making this non-fatal is a bit of a kludge (see dirs2
+          in testsuite).  A better solution would be to avoid having
+          the server tell us about a directory we shouldn't be doing
+          anything with anyway (e.g. by handling directory
+          addition/removal better).  */
+       error (0, errno, "cannot open %s", CVSADM_TAG);
+       free (tagspec);
+       return;
+    }
+    if (fprintf (f, "%s\n", tagspec) < 0)
+       error (1, errno, "writing %s", CVSADM_TAG);
+    if (fclose (f) == EOF)
+       error (1, errno, "closing %s", CVSADM_TAG);
+    free (tagspec);
+}
+
+
+
+static void
+handle_set_sticky (char *pathname, size_t len)
+{
+    if (!strcmp (cvs_cmd_name, "export"))
+    {
+       /* Swallow the repository.  */
+       read_line (NULL);
+        /* Swallow the tag line.  */
+       read_line (NULL);
+       return;
+    }
+    if (is_cvsroot_level (pathname))
+    {
+        /*
+        * Top level (directory containing CVSROOT).  This seems to normally
+        * lack a CVS directory, so don't try to create files in it.
+        */
+
+       /* Swallow the repository.  */
+       read_line (NULL);
+        /* Swallow the tag line.  */
+       read_line (NULL);
+       return;
+    }
+
+    call_in_directory (pathname, set_sticky, NULL);
+}
+
+
+
+static void
+clear_sticky (void *data, List *ent_list, const char *short_pathname,
+              const char *filename)
+{
+    if (unlink_file (CVSADM_TAG) < 0 && ! existence_error (errno))
+       error (1, errno, "cannot remove %s", CVSADM_TAG);
+}
+
+
+
+static void
+handle_clear_sticky (char *pathname, size_t len)
+{
+    if (!strcmp (cvs_cmd_name, "export"))
+    {
+       /* Swallow the repository.  */
+       read_line (NULL);
+       return;
+    }
+
+    if (is_cvsroot_level (pathname))
+    {
+        /*
+        * Top level (directory containing CVSROOT).  This seems to normally
+        * lack a CVS directory, so don't try to create files in it.
+        */
+       return;
+    }
+
+    call_in_directory (pathname, clear_sticky, NULL);
+}
+
+
+
+/* Handle the client-side support for a successful edit.
+ */
+static void
+handle_edit_file (char *pathname, size_t len)
+{
+    call_in_directory (pathname, edit_file, NULL);
+}
+
+
+
+static void
+template (void *data, List *ent_list, const char *short_pathname,
+         const char *filename)
+{
+    char *buf = Xasprintf ("%s/%s", short_pathname, CVSADM_TEMPLATE);
+    read_counted_file (CVSADM_TEMPLATE, buf);
+    free (buf);
+}
+
+
+
+static void
+handle_template (char *pathname, size_t len)
+{
+    call_in_directory (pathname, template, NULL);
+}
+
+
+
+static void
+clear_template (void *data, List *ent_list, const char *short_pathname,
+                const char *filename)
+{
+    if (unlink_file (CVSADM_TEMPLATE) < 0 && ! existence_error (errno))
+       error (1, errno, "cannot remove %s", CVSADM_TEMPLATE);
+}
+
+
+
+static void
+handle_clear_template (char *pathname, size_t len)
+{
+    call_in_directory (pathname, clear_template, NULL);
+}
+
+
+
+struct save_dir {
+    char *dir;
+    struct save_dir *next;
+};
+
+struct save_dir *prune_candidates;
+
+static void
+add_prune_candidate (const char *dir)
+{
+    struct save_dir *p;
+
+    if ((dir[0] == '.' && dir[1] == '\0')
+       || (prune_candidates && !strcmp (dir, prune_candidates->dir)))
+       return;
+    p = xmalloc (sizeof (struct save_dir));
+    p->dir = xstrdup (dir);
+    p->next = prune_candidates;
+    prune_candidates = p;
+}
+
+
+
+static void
+process_prune_candidates (void)
+{
+    struct save_dir *p;
+    struct save_dir *q;
+
+    if (toplevel_wd)
+    {
+       if (CVS_CHDIR (toplevel_wd) < 0)
+           error (1, errno, "could not chdir to %s", toplevel_wd);
+    }
+    for (p = prune_candidates; p; )
+    {
+       if (isemptydir (p->dir, 1))
+       {
+           char *b;
+
+           if (unlink_file_dir (p->dir) < 0)
+               error (0, errno, "cannot remove %s", p->dir);
+           b = strrchr (p->dir, '/');
+           if (!b)
+               Subdir_Deregister (NULL, NULL, p->dir);
+           else
+           {
+               *b = '\0';
+               Subdir_Deregister (NULL, p->dir, b + 1);
+           }
+       }
+       free (p->dir);
+       q = p->next;
+       free (p);
+       p = q;
+    }
+    prune_candidates = NULL;
+}
+
+
+
+/* Send a Repository line.  */
+static char *last_repos;
+static char *last_update_dir;
+static void
+send_repository (const char *dir, const char *repos, const char *update_dir)
+{
+    char *adm_name;
+
+    /* FIXME: this is probably not the best place to check; I wish I
+     * knew where in here's callers to really trap this bug.  To
+     * reproduce the bug, just do this:
+     * 
+     *       mkdir junk
+     *       cd junk
+     *       cvs -d some_repos update foo
+     *
+     * Poof, CVS seg faults and dies!  It's because it's trying to
+     * send a NULL string to the server but dies in send_to_server.
+     * That string was supposed to be the repository, but it doesn't
+     * get set because there's no CVSADM dir, and somehow it's not
+     * getting set from the -d argument either... ?
+     */
+    if (!repos)
+    {
+        /* Lame error.  I want a real fix but can't stay up to track
+           this down right now. */
+        error (1, 0, "no repository");
+    }
+
+    if (!update_dir || update_dir[0] == '\0')
+       update_dir = ".";
+
+    if (last_repos && !strcmp (repos, last_repos)
+       && last_update_dir && !strcmp (update_dir, last_update_dir))
+       /* We've already sent it.  */
+       return;
+
+    if (client_prune_dirs)
+       add_prune_candidate (update_dir);
+
+    /* Add a directory name to the list of those sent to the
+       server. */
+    if (update_dir && *update_dir != '\0' && strcmp (update_dir, ".")
+       && !findnode (dirs_sent_to_server, update_dir))
+    {
+       Node *n;
+       n = getnode ();
+       n->type = NT_UNKNOWN;
+       n->key = xstrdup (update_dir);
+       n->data = NULL;
+
+       if (addnode (dirs_sent_to_server, n))
+           error (1, 0, "cannot add directory %s to list", n->key);
+    }
+
+    /* 80 is large enough for any of CVSADM_*.  */
+    adm_name = xmalloc (strlen (dir) + 80);
+
+    send_to_server ("Directory ", 0);
+    {
+       /* Send the directory name.  I know that this
+          sort of duplicates code elsewhere, but each
+          case seems slightly different...  */
+       char buf[1];
+       const char *p = update_dir;
+       while (*p != '\0')
+       {
+           assert (*p != '\012');
+           if (ISSLASH (*p))
+           {
+               buf[0] = '/';
+               send_to_server (buf, 1);
+           }
+           else
+           {
+               buf[0] = *p;
+               send_to_server (buf, 1);
+           }
+           ++p;
+       }
+    }
+    send_to_server ("\012", 1);
+    if (supported_request ("Relative-directory"))
+    {
+       const char *short_repos = Short_Repository (repos);
+       send_to_server (short_repos, 0);
+    }
+    else
+       send_to_server (repos, 0);
+    send_to_server ("\012", 1);
+
+    if (supported_request ("Static-directory"))
+    {
+       adm_name[0] = '\0';
+       if (dir[0] != '\0')
+       {
+           strcat (adm_name, dir);
+           strcat (adm_name, "/");
+       }
+       strcat (adm_name, CVSADM_ENTSTAT);
+       if (isreadable (adm_name))
+       {
+           send_to_server ("Static-directory\012", 0);
+       }
+    }
+    if (supported_request ("Sticky"))
+    {
+       FILE *f;
+       if (dir[0] == '\0')
+           strcpy (adm_name, CVSADM_TAG);
+       else
+           sprintf (adm_name, "%s/%s", dir, CVSADM_TAG);
+
+       f = CVS_FOPEN (adm_name, "r");
+       if (!f)
+       {
+           if (! existence_error (errno))
+               error (1, errno, "reading %s", adm_name);
+       }
+       else
+       {
+           char line[80];
+           char *nl = NULL;
+           send_to_server ("Sticky ", 0);
+           while (fgets (line, sizeof (line), f))
+           {
+               send_to_server (line, 0);
+               nl = strchr (line, '\n');
+               if (nl)
+                   break;
+           }
+           if (!nl)
+                send_to_server ("\012", 1);
+           if (fclose (f) == EOF)
+               error (0, errno, "closing %s", adm_name);
+       }
+    }
+    free (adm_name);
+    if (last_repos) free (last_repos);
+    if (last_update_dir) free (last_update_dir);
+    last_repos = xstrdup (repos);
+    last_update_dir = xstrdup (update_dir);
+}
+
+
+
+/* Send a Repository line and set toplevel_repos.  */
+void
+send_a_repository (const char *dir, const char *repository,
+                   const char *update_dir_in)
+{
+    char *update_dir = xstrdup (update_dir_in);
+
+    if (!toplevel_repos && repository)
+    {
+       if (update_dir[0] == '\0'
+           || (update_dir[0] == '.' && update_dir[1] == '\0'))
+           toplevel_repos = xstrdup (repository);
+       else
+       {
+           /*
+            * Get the repository from a CVS/Repository file if update_dir
+            * is absolute.  This is not correct in general, because
+            * the CVS/Repository file might not be the top-level one.
+            * This is for cases like "cvs update /foo/bar" (I'm not
+            * sure it matters what toplevel_repos we get, but it does
+            * matter that we don't hit the "internal error" code below).
+            */
+           if (update_dir[0] == '/')
+               toplevel_repos = Name_Repository (update_dir, update_dir);
+           else
+           {
+               /*
+                * Guess the repository of that directory by looking at a
+                * subdirectory and removing as many pathname components
+                * as are in update_dir.  I think that will always (or at
+                * least almost always) be 1.
+                *
+                * So this deals with directories which have been
+                * renamed, though it doesn't necessarily deal with
+                * directories which have been put inside other
+                * directories (and cvs invoked on the containing
+                * directory).  I'm not sure the latter case needs to
+                * work.
+                *
+                * 21 Aug 1998: Well, Mr. Above-Comment-Writer, it
+                * does need to work after all.  When we are using the
+                * client in a multi-cvsroot environment, it will be
+                * fairly common that we have the above case (e.g.,
+                * cwd checked out from one repository but
+                * subdirectory checked out from another).  We can't
+                * assume that by walking up a directory in our wd we
+                * necessarily walk up a directory in the repository.
+                */
+               /*
+                * This gets toplevel_repos wrong for "cvs update ../foo"
+                * but I'm not sure toplevel_repos matters in that case.
+                */
+
+               int repository_len, update_dir_len;
+
+               strip_trailing_slashes (update_dir);
+
+               repository_len = strlen (repository);
+               update_dir_len = strlen (update_dir);
+
+               /* Try to remove the path components in UPDATE_DIR
+                   from REPOSITORY.  If the path elements don't exist
+                   in REPOSITORY, or the removal of those path
+                   elements mean that we "step above"
+                   current_parsed_root->directory, set toplevel_repos to
+                   current_parsed_root->directory. */
+               if (repository_len > update_dir_len
+                   && !strcmp (repository + repository_len - update_dir_len,
+                               update_dir)
+                   /* TOPLEVEL_REPOS shouldn't be above 
current_parsed_root->directory */
+                   && ((size_t)(repository_len - update_dir_len)
+                       > strlen (current_parsed_root->directory)))
+               {
+                   /* The repository name contains UPDATE_DIR.  Set
+                       toplevel_repos to the repository name without
+                       UPDATE_DIR. */
+
+                   toplevel_repos = xmalloc (repository_len - update_dir_len);
+                   /* Note that we don't copy the trailing '/'.  */
+                   strncpy (toplevel_repos, repository,
+                            repository_len - update_dir_len - 1);
+                   toplevel_repos[repository_len - update_dir_len - 1] = '\0';
+               }
+               else
+               {
+                   toplevel_repos = xstrdup (current_parsed_root->directory);
+               }
+           }
+       }
+    }
+
+    send_repository (dir, repository, update_dir);
+    free (update_dir);
+}
+
+
+
+static void
+notified_a_file (void *data, List *ent_list, const char *short_pathname,
+                 const char *filename)
+{
+    FILE *fp;
+    FILE *newf;
+    size_t line_len = 8192;
+    char *line = xmalloc (line_len);
+    char *cp;
+    int nread;
+    int nwritten;
+    char *p;
+
+    fp = xfopen (CVSADM_NOTIFY, "r");
+    if (getline (&line, &line_len, fp) < 0)
+    {
+       if (feof (fp))
+           error (0, 0, "cannot read %s: end of file", CVSADM_NOTIFY);
+       else
+           error (0, errno, "cannot read %s", CVSADM_NOTIFY);
+       goto error_exit;
+    }
+    cp = strchr (line, '\t');
+    if (!cp)
+    {
+       error (0, 0, "malformed %s file", CVSADM_NOTIFY);
+       goto error_exit;
+    }
+    *cp = '\0';
+    if (strcmp (filename, line + 1))
+       error (0, 0, "protocol error: notified %s, expected %s", filename,
+              line + 1);
+
+    if (getline (&line, &line_len, fp) < 0)
+    {
+       if (feof (fp))
+       {
+           free (line);
+           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;
+       }
+       else
+       {
+           error (0, errno, "cannot read %s", CVSADM_NOTIFY);
+           goto error_exit;
+       }
+    }
+    newf = xfopen (CVSADM_NOTIFYTMP, "w");
+    if (fputs (line, newf) < 0)
+    {
+       error (0, errno, "cannot write %s", CVSADM_NOTIFYTMP);
+       goto error2;
+    }
+    while ((nread = fread (line, 1, line_len, fp)) > 0)
+    {
+       p = line;
+       while ((nwritten = fwrite (p, sizeof *p, nread, newf)) > 0)
+       {
+           nread -= nwritten;
+           p += nwritten;
+       }
+       if (ferror (newf))
+       {
+           error (0, errno, "cannot write %s", CVSADM_NOTIFYTMP);
+           goto error2;
+       }
+    }
+    if (ferror (fp))
+    {
+       error (0, errno, "cannot read %s", CVSADM_NOTIFY);
+       goto error2;
+    }
+    if (fclose (newf) < 0)
+    {
+       error (0, errno, "cannot close %s", CVSADM_NOTIFYTMP);
+       goto error_exit;
+    }
+    free (line);
+    if (fclose (fp) < 0)
+    {
+       error (0, errno, "cannot close %s", CVSADM_NOTIFY);
+       return;
+    }
+
+    {
+        /* In this case, we want rename_file() to ignore noexec. */
+        int saved_noexec = noexec;
+        noexec = 0;
+        rename_file (CVSADM_NOTIFYTMP, CVSADM_NOTIFY);
+        noexec = saved_noexec;
+    }
+
+    return;
+  error2:
+    (void)fclose (newf);
+  error_exit:
+    free (line);
+    (void)fclose (fp);
+}
+
+
+
+static void
+handle_notified (char *args, size_t len)
+{
+    call_in_directory (args, notified_a_file, NULL);
+}
+
+
+
+/* The "expanded" modules.  */
+static int modules_count;
+static int modules_allocated;
+static char **modules_vector;
+
+static void
+handle_module_expansion (char *args, size_t len)
+{
+    if (!modules_vector)
+    {
+       modules_allocated = 1; /* Small for testing */
+       modules_vector = xnmalloc (modules_allocated,
+                                  sizeof (modules_vector[0]));
+    }
+    else if (modules_count >= modules_allocated)
+    {
+       modules_allocated *= 2;
+       modules_vector = xnrealloc (modules_vector,
+                                   modules_allocated,
+                                   sizeof (modules_vector[0]));
+    }
+    modules_vector[modules_count] = xstrdup (args);
+    ++modules_count;
+}
+
+
+
+/* Original, not "expanded" modules.  */
+static int module_argc;
+static char **module_argv;
+
+void
+client_expand_modules (int argc, char **argv, int local)
+{
+    int errs;
+    int i;
+
+    module_argc = argc;
+    module_argv = xnmalloc (argc + 1, sizeof (module_argv[0]));
+    for (i = 0; i < argc; ++i)
+       module_argv[i] = xstrdup (argv[i]);
+    module_argv[argc] = NULL;
+
+    for (i = 0; i < argc; ++i)
+       send_arg (argv[i]);
+    send_a_repository ("", current_parsed_root->directory, "");
+
+    send_to_server ("expand-modules\012", 0);
+
+    errs = get_server_responses ();
+
+    if (last_repos) free (last_repos);
+    last_repos = NULL;
+
+    if (last_update_dir) free (last_update_dir);
+    last_update_dir = NULL;
+
+    if (errs)
+       error (errs, 0, "cannot expand modules");
+}
+
+
+
+void
+client_send_expansions (int local, char *where, int build_dirs)
+{
+    int i;
+    char *argv[1];
+
+    /* Send the original module names.  The "expanded" module name might
+       not be suitable as an argument to a co request (e.g. it might be
+       the result of a -d argument in the modules file).  It might be
+       cleaner if we genuinely expanded module names, all the way to a
+       local directory and repository, but that isn't the way it works
+       now.  */
+    send_file_names (module_argc, module_argv, 0);
+
+    for (i = 0; i < modules_count; ++i)
+    {
+       argv[0] = where ? where : modules_vector[i];
+       if (isfile (argv[0]))
+           send_files (1, argv, local, 0, build_dirs ? SEND_BUILD_DIRS : 0);
+    }
+    send_a_repository ("", current_parsed_root->directory, "");
+}
+
+
+
+void
+client_nonexpanded_setup (void)
+{
+    send_a_repository ("", current_parsed_root->directory, "");
+}
+
+
+
+/* Receive a cvswrappers line from the server; it must be a line
+   containing an RCS option (e.g., "*.exe   -k 'b'").
+
+   Note that this doesn't try to handle -t/-f options (which are a
+   whole separate issue which noone has thought much about, as far
+   as I know).
+
+   We need to know the keyword expansion mode so we know whether to
+   read the file in text or binary mode.  */
+static void
+handle_wrapper_rcs_option (char *args, size_t len)
+{
+    char *p;
+
+    /* Enforce the notes in cvsclient.texi about how the response is not
+       as free-form as it looks.  */
+    p = strchr (args, ' ');
+    if (!p)
+       goto handle_error;
+    if (*++p != '-'
+       || *++p != 'k'
+       || *++p != ' '
+       || *++p != '\'')
+       goto handle_error;
+    if (!strchr (p, '\''))
+       goto handle_error;
+
+    /* Add server-side cvswrappers line to our wrapper list. */
+    wrap_add (args, 0);
+    return;
+ handle_error:
+    error (0, errno, "protocol error: ignoring invalid wrappers %s", args);
+}
+
+
+
+
+static void
+handle_m (char *args, size_t len)
+{
+    /* In the case where stdout and stderr point to the same place,
+       fflushing stderr will make output happen in the correct order.
+       Often stderr will be line-buffered and this won't be needed,
+       but not always (is that true?  I think the comment is probably
+       based on being confused between default buffering between
+       stdout and stderr.  But I'm not sure).  */
+    fflush (stderr);
+    fwrite (args, sizeof *args, len, stdout);
+    putc ('\n', stdout);
+}
+
+
+
+static void
+handle_mbinary (char *args, size_t len)
+{
+    char *size_string;
+    size_t size;
+    size_t totalread;
+    size_t nread;
+    size_t toread;
+    char buf[8192];
+
+    /* See comment at handle_m about (non)flush of stderr.  */
+
+    /* Get the size.  */
+    read_line (&size_string);
+    size = atoi (size_string);
+    free (size_string);
+
+    /* OK, now get all the data.  The algorithm here is that we read
+       as much as the network wants to give us in
+       try_read_from_server, and then we output it all, and then
+       repeat, until we get all the data.  */
+    totalread = 0;
+    while (totalread < size)
+    {
+       toread = size - totalread;
+       if (toread > sizeof buf)
+           toread = sizeof buf;
+
+       nread = try_read_from_server (buf, toread);
+       cvs_output_binary (buf, nread);
+       totalread += nread;
+    }
+}
+
+
+
+static void
+handle_e (char *args, size_t len)
+{
+    /* In the case where stdout and stderr point to the same place,
+       fflushing stdout will make output happen in the correct order.  */
+    fflush (stdout);
+    fwrite (args, sizeof *args, len, stderr);
+    putc ('\n', stderr);
+}
+
+
+
+/*ARGSUSED*/
+static void
+handle_f  (char *args, size_t len)
+{
+    fflush (stderr);
+}
+
+
+
+static void
+handle_mt (char *args, size_t len)
+{
+    char *p;
+    char *tag = args;
+    char *text;
+
+    /* See comment at handle_m for more details.  */
+    fflush (stderr);
+
+    p = strchr (args, ' ');
+    if (!p)
+       text = NULL;
+    else
+    {
+       *p++ = '\0';
+       text = p;
+    }
+
+    switch (tag[0])
+    {
+       case '+':
+           if (!strcmp (tag, "+updated"))
+               updated_seen = 1;
+           else if (!strcmp (tag, "+importmergecmd"))
+               importmergecmd.seen = 1;
+           break;
+       case '-':
+           if (!strcmp (tag, "-updated"))
+               updated_seen = 0;
+           else if (!strcmp (tag, "-importmergecmd"))
+           {
+               char buf[80];
+
+               /* Now that we have gathered the information, we can
+                   output the suggested merge command.  */
+
+               if (importmergecmd.conflicts == 0
+                   || !importmergecmd.mergetag1
+                   || !importmergecmd.mergetag2
+                   || !importmergecmd.repository)
+               {
+                   error (0, 0,
+                          "invalid server: incomplete importmergecmd tags");
+                   break;
+               }
+
+               if (importmergecmd.conflicts == -1)
+                   sprintf (buf, "\nNo conflicts created by this import.\n");
+               else
+                   sprintf (buf, "\n%d conflicts created by this import.\n",
+                            importmergecmd.conflicts);
+               cvs_output (buf, 0);
+               cvs_output ("Use the following command to help the merge:\n\n",
+                           0);
+               cvs_output ("\t", 1);
+               cvs_output (program_name, 0);
+               if (CVSroot_cmdline)
+               {
+                   cvs_output (" -d ", 0);
+                   cvs_output (CVSroot_cmdline, 0);
+               }
+               cvs_output (" checkout -j", 0);
+               cvs_output (importmergecmd.mergetag1, 0);
+               cvs_output (" -j", 0);
+               cvs_output (importmergecmd.mergetag2, 0);
+               cvs_output (" ", 1);
+               cvs_output (importmergecmd.repository, 0);
+               cvs_output ("\n\n", 0);
+
+               /* Clear the static variables so that everything is
+                   ready for any subsequent importmergecmd tag.  */
+               importmergecmd.conflicts = 0;
+               free (importmergecmd.mergetag1);
+               importmergecmd.mergetag1 = NULL;
+               free (importmergecmd.mergetag2);
+               importmergecmd.mergetag2 = NULL;
+               free (importmergecmd.repository);
+               importmergecmd.repository = NULL;
+
+               importmergecmd.seen = 0;
+           }
+           break;
+       default:
+           if (updated_seen)
+           {
+               if (!strcmp (tag, "fname"))
+               {
+                   if (updated_fname)
+                   {
+                       /* Output the previous message now.  This can happen
+                          if there was no Update-existing or other such
+                          response, due to the -n global option.  */
+                       cvs_output ("U ", 0);
+                       cvs_output (updated_fname, 0);
+                       cvs_output ("\n", 1);
+                       free (updated_fname);
+                   }
+                   updated_fname = xstrdup (text);
+               }
+               /* Swallow all other tags.  Either they are extraneous
+                  or they reflect future extensions that we can
+                  safely ignore.  */
+           }
+           else if (importmergecmd.seen)
+           {
+               if (!strcmp (tag, "conflicts"))
+               {
+                   if (!strcmp (text, "No"))
+                       importmergecmd.conflicts = -1;
+                   else
+                       importmergecmd.conflicts = atoi (text);
+               }
+               else if (!strcmp (tag, "mergetag1"))
+                   importmergecmd.mergetag1 = xstrdup (text);
+               else if (!strcmp (tag, "mergetag2"))
+                   importmergecmd.mergetag2 = xstrdup (text);
+               else if (!strcmp (tag, "repository"))
+                   importmergecmd.repository = xstrdup (text);
+               /* Swallow all other tags.  Either they are text for
+                   which we are going to print our own version when we
+                   see -importmergecmd, or they are future extensions
+                   we can safely ignore.  */
+           }
+           else if (!strcmp (tag, "newline"))
+               printf ("\n");
+           else if (!strcmp (tag, "date"))
+           {
+               char *date = format_date_alloc (text);
+               printf ("%s", date);
+               free (date);
+           }
+           else if (text)
+               printf ("%s", text);
+    }
+}
+
+
+
+#endif /* CLIENT_SUPPORT */
+#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT)
+
+/* This table must be writeable if the server code is included.  */
+struct response responses[] =
+{
+#ifdef CLIENT_SUPPORT
+#define RSP_LINE(n, f, t, s) {n, f, t, s}
+#else /* ! CLIENT_SUPPORT */
+#define RSP_LINE(n, f, t, s) {n, s}
+#endif /* CLIENT_SUPPORT */
+
+    RSP_LINE("ok", handle_ok, response_type_ok, rs_essential),
+    RSP_LINE("error", handle_error, response_type_error, rs_essential),
+    RSP_LINE("Valid-requests", handle_valid_requests, response_type_normal,
+       rs_essential),
+    RSP_LINE("Force-gzip", handle_force_gzip, response_type_normal,
+       rs_optional),
+    RSP_LINE("Referrer", handle_referrer, response_type_normal, rs_optional),
+    RSP_LINE("Redirect", handle_redirect, response_type_redirect, rs_optional),
+    RSP_LINE("Checked-in", handle_checked_in, response_type_normal,
+       rs_essential),
+    RSP_LINE("New-entry", handle_new_entry, response_type_normal, rs_optional),
+    RSP_LINE("Checksum", handle_checksum, response_type_normal, rs_optional),
+    RSP_LINE("Copy-file", handle_copy_file, response_type_normal, rs_optional),
+    RSP_LINE("Updated", handle_updated, response_type_normal, rs_essential),
+    RSP_LINE("Created", handle_created, response_type_normal, rs_optional),
+    RSP_LINE("Update-existing", handle_update_existing, response_type_normal,
+       rs_optional),
+    RSP_LINE("Merged", handle_merged, response_type_normal, rs_essential),
+    RSP_LINE("Patched", handle_patched, response_type_normal, rs_optional),
+    RSP_LINE("Rcs-diff", handle_rcs_diff, response_type_normal, rs_optional),
+    RSP_LINE("Mode", handle_mode, response_type_normal, rs_optional),
+    RSP_LINE("Mod-time", handle_mod_time, response_type_normal, rs_optional),
+    RSP_LINE("Removed", handle_removed, response_type_normal, rs_essential),
+    RSP_LINE("Remove-entry", handle_remove_entry, response_type_normal,
+       rs_optional),
+    RSP_LINE("Set-static-directory", handle_set_static_directory,
+       response_type_normal,
+       rs_optional),
+    RSP_LINE("Clear-static-directory", handle_clear_static_directory,
+       response_type_normal,
+       rs_optional),
+    RSP_LINE("Set-sticky", handle_set_sticky, response_type_normal,
+       rs_optional),
+    RSP_LINE("Clear-sticky", handle_clear_sticky, response_type_normal,
+       rs_optional),
+    RSP_LINE("Edit-file", handle_edit_file, response_type_normal,
+       rs_optional),
+    RSP_LINE("Template", handle_template, response_type_normal,
+       rs_optional),
+    RSP_LINE("Clear-template", handle_clear_template, response_type_normal,
+       rs_optional),
+    RSP_LINE("Notified", handle_notified, response_type_normal, rs_optional),
+    RSP_LINE("Module-expansion", handle_module_expansion, response_type_normal,
+       rs_optional),
+    RSP_LINE("Wrapper-rcsOption", handle_wrapper_rcs_option,
+       response_type_normal,
+       rs_optional),
+    RSP_LINE("M", handle_m, response_type_normal, rs_essential),
+    RSP_LINE("Mbinary", handle_mbinary, response_type_normal, rs_optional),
+    RSP_LINE("E", handle_e, response_type_normal, rs_essential),
+    RSP_LINE("F", handle_f, response_type_normal, rs_optional),
+    RSP_LINE("MT", handle_mt, response_type_normal, rs_optional),
+    /* Possibly should be response_type_error.  */
+    RSP_LINE(NULL, NULL, response_type_normal, rs_essential)
+
+#undef RSP_LINE
+};
+
+#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */
+#ifdef CLIENT_SUPPORT
+
+
+
+/* 
+ * If LEN is 0, then send_to_server_via() computes string's length itself.
+ *
+ * Therefore, pass the real length when transmitting data that might
+ * contain 0's.
+ */
+void
+send_to_server_via (struct buffer *via_buffer, const char *str, size_t len)
+{
+    static int nbytes;
+
+    if (len == 0)
+       len = strlen (str);
+
+    buf_output (via_buffer, str, len);
+      
+    /* There is no reason not to send data to the server, so do it
+       whenever we've accumulated enough information in the buffer to
+       make it worth sending.  */
+    nbytes += len;
+    if (nbytes >= 2 * BUFFER_DATA_SIZE)
+    {
+       int status;
+
+        status = buf_send_output (via_buffer);
+       if (status != 0)
+           error (1, status, "error writing to server");
+       nbytes = 0;
+    }
+}
+
+
+
+void
+send_to_server (const char *str, size_t len)
+{
+  send_to_server_via (global_to_server, str, len);
+}
+
+
+
+/* Read up to LEN bytes from the server.  Returns actual number of
+   bytes read, which will always be at least one; blocks if there is
+   no data available at all.  Gives a fatal error on EOF or error.  */
+static size_t
+try_read_from_server( char *buf, size_t len )
+{
+    int status;
+    size_t nread;
+    char *data;
+
+    status = buf_read_data (global_from_server, len, &data, &nread);
+    if (status != 0)
+    {
+       if (status == -1)
+           error (1, 0,
+                  "end of file from server (consult above messages if any)");
+       else if (status == -2)
+           error (1, 0, "out of memory");
+       else
+           error (1, status, "reading from server");
+    }
+
+    memcpy (buf, data, nread);
+
+    return nread;
+}
+
+
+
+/*
+ * Read LEN bytes from the server or die trying.
+ */
+void
+read_from_server (char *buf, size_t len)
+{
+    size_t red = 0;
+    while (red < len)
+    {
+       red += try_read_from_server (buf + red, len - red);
+       if (red == len)
+           break;
+    }
+}
+
+
+
+/* Get some server responses and process them.
+ *
+ * RETURNS
+ *   0         Success
+ *   1         Error
+ *   2         Redirect
+ */
+int
+get_server_responses (void)
+{
+    struct response *rs;
+    do
+    {
+       char *cmd;
+       size_t len;
+       
+       len = read_line (&cmd);
+       for (rs = responses; rs->name; ++rs)
+           if (!strncmp (cmd, rs->name, strlen (rs->name)))
+           {
+               size_t cmdlen = strlen (rs->name);
+               if (cmd[cmdlen] == '\0')
+                   ;
+               else if (cmd[cmdlen] == ' ')
+                   ++cmdlen;
+               else
+                   /*
+                    * The first len characters match, but it's a different
+                    * response.  e.g. the response is "oklahoma" but we
+                    * matched "ok".
+                    */
+                   continue;
+               (*rs->func) (cmd + cmdlen, len - cmdlen);
+               break;
+           }
+       if (!rs->name)
+           /* It's OK to print just to the first '\0'.  */
+           /* We might want to handle control characters and the like
+              in some other way other than just sending them to stdout.
+              One common reason for this error is if people use :ext:
+              with a version of rsh which is doing CRLF translation or
+              something, and so the client gets "ok^M" instead of "ok".
+              Right now that will tend to print part of this error
+              message over the other part of it.  It seems like we could
+              do better (either in general, by quoting or omitting all
+              control characters, and/or specifically, by detecting the CRLF
+              case and printing a specific error message).  */
+           error (0, 0,
+                  "warning: unrecognized response `%s' from cvs server",
+                  cmd);
+       free (cmd);
+    } while (rs->type == response_type_normal);
+
+    if (updated_fname)
+    {
+       /* Output the previous message now.  This can happen
+          if there was no Update-existing or other such
+          response, due to the -n global option.  */
+       cvs_output ("U ", 0);
+       cvs_output (updated_fname, 0);
+       cvs_output ("\n", 1);
+       free (updated_fname);
+       updated_fname = NULL;
+    }
+
+    if (rs->type == response_type_redirect) return 2;
+    if (rs->type == response_type_error) return 1;
+    if (failure_exit) return 1;
+    return 0;
+}
+
+
+
+static inline void
+close_connection_to_server (struct buffer **to, struct buffer **from)
+{
+    int status;
+
+    /* First we shut down GLOBAL_TO_SERVER.  That tells the server that its
+     * input is finished.  It then shuts down the buffer it is sending to us,
+     * at which point our shut down of GLOBAL_FROM_SERVER will complete.
+     */
+
+    TRACE (TRACE_FUNCTION, "close_connection_to_server ()");
+
+    status = buf_shutdown (*to);
+    if (status != 0)
+       error (0, status, "shutting down buffer to server");
+    buf_free (*to);
+    *to = NULL;
+
+    status = buf_shutdown (*from);
+    if (status != 0)
+       error (0, status, "shutting down buffer from server");
+    buf_free (*from);
+    *from = NULL;
+}
+
+
+
+/* Get the responses and then close the connection.  */
+
+/*
+ * Flag var; we'll set it in start_server() and not one of its
+ * callees, such as start_rsh_server().  This means that there might
+ * be a small window between the starting of the server and the
+ * setting of this var, but all the code in that window shouldn't care
+ * because it's busy checking return values to see if the server got
+ * started successfully anyway.
+ */
+int server_started = 0;
+
+int
+get_responses_and_close (void)
+{
+    int errs = get_server_responses ();
+
+    /* The following is necessary when working with multiple cvsroots, at least
+     * with commit.  It used to be buried nicely in do_deferred_progs() before
+     * that function was removed.  I suspect it wouldn't be necessary if
+     * call_in_directory() saved its working directory via save_cwd() before
+     * changing its directory and restored the saved working directory via
+     * restore_cwd() before exiting.  Of course, calling CVS_CHDIR only once,
+     * here, may be more efficient.
+     */
+    if (toplevel_wd)
+    {
+       if (CVS_CHDIR (toplevel_wd) < 0)
+           error (1, errno, "could not chdir to %s", toplevel_wd);
+    }
+
+    if (client_prune_dirs)
+       process_prune_candidates ();
+
+    close_connection_to_server (&global_to_server, &global_from_server);
+    server_started = 0;
+
+    /* see if we need to sleep before returning to avoid time-stamp races */
+    if (last_register_time)
+       sleep_past (last_register_time);
+
+    return errs;
+}
+
+
+
+bool
+supported_request (const char *name)
+{
+    struct request *rq;
+
+    for (rq = requests; rq->name; rq++)
+       if (!strcmp (rq->name, name))
+           return (rq->flags & RQ_SUPPORTED) != 0;
+    error (1, 0, "internal error: testing support for unknown request?");
+    /* NOTREACHED */
+    return 0;
+}
+
+
+
+#if defined (AUTH_CLIENT_SUPPORT) || defined (SERVER_SUPPORT) || defined 
(HAVE_KERBEROS) || defined (HAVE_GSSAPI)
+
+
+/* Generic function to do port number lookup tasks.
+ *
+ * In order of precedence, will return:
+ *     getenv (envname), if defined
+ *     getservbyname (portname), if defined
+ *     defaultport
+ */
+static int
+get_port_number (const char *envname, const char *portname, int defaultport)
+{
+    struct servent *s;
+    char *port_s;
+
+    if (envname && (port_s = getenv (envname)))
+    {
+       int port = atoi (port_s);
+       if (port <= 0)
+       {
+           error (0, 0, "%s must be a positive integer!  If you", envname);
+           error (0, 0, "are trying to force a connection via rsh, please");
+           error (0, 0, "put \":server:\" at the beginning of your CVSROOT");
+           error (1, 0, "variable.");
+       }
+       return port;
+    }
+    else if (portname && (s = getservbyname (portname, "tcp")))
+       return ntohs (s->s_port);
+    else
+       return defaultport;
+}
+
+
+
+/* get the port number for a client to connect to based on the port
+ * and method of a cvsroot_t.
+ *
+ * we do this here instead of in parse_cvsroot so that we can keep network
+ * code confined to a localized area and also to delay the lookup until the
+ * last possible moment so it remains possible to run cvs client commands that
+ * skip opening connections to the server (i.e. skip network operations
+ * entirely)
+ *
+ * and yes, I know none of the commands do that now, but here's to planning
+ * for the future, eh?  cheers.
+ */
+int
+get_cvs_port_number (const cvsroot_t *root)
+{
+
+    if (root->port) return root->port;
+
+    switch (root->method)
+    {
+# ifdef HAVE_GSSAPI
+       case gserver_method:
+# endif /* HAVE_GSSAPI */
+# ifdef AUTH_CLIENT_SUPPORT
+       case pserver_method:
+# endif /* AUTH_CLIENT_SUPPORT */
+# if defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_GSSAPI)
+           return get_port_number ("CVS_CLIENT_PORT", "cvspserver",
+                                    CVS_AUTH_PORT);
+# endif /* defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_GSSAPI) */
+# ifdef HAVE_KERBEROS
+       case kserver_method:
+           return get_port_number ("CVS_CLIENT_PORT", "cvs", CVS_PORT);
+# endif /* HAVE_KERBEROS */
+       default:
+           error(1, EINVAL,
+"internal error: get_cvs_port_number called for invalid connection method 
(%s)",
+                 method_names[root->method]);
+           break;
+    }
+    /* NOTREACHED */
+    return -1;
+}
+
+
+
+/* get the port number for a client to connect to based on the proxy port
+ * of a cvsroot_t.
+ */
+static int
+get_proxy_port_number (const cvsroot_t *root)
+{
+
+    if (root->proxy_port) return root->proxy_port;
+
+    return get_port_number ("CVS_PROXY_PORT", NULL, CVS_PROXY_PORT);
+}
+
+
+
+void
+make_bufs_from_fds(int tofd, int fromfd, int child_pid, cvsroot_t *root,
+                   struct buffer **to_server_p,
+                   struct buffer **from_server_p, int is_sock)
+{
+# ifdef NO_SOCKET_TO_FD
+    if (is_sock)
+    {
+       assert (tofd == fromfd);
+       *to_server_p = socket_buffer_initialize (tofd, 0, NULL);
+       *from_server_p = socket_buffer_initialize (tofd, 1, NULL);
+    }
+    else
+# endif /* NO_SOCKET_TO_FD */
+    {
+       /* todo: some OS's don't need these calls... */
+       close_on_exec (tofd);
+       close_on_exec (fromfd);
+
+       /* SCO 3 and AIX have a nasty bug in the I/O libraries which precludes
+          fdopening the same file descriptor twice, so dup it if it is the
+          same.  */
+       if (tofd == fromfd)
+       {
+           fromfd = dup (tofd);
+           if (fromfd < 0)
+               error (1, errno, "cannot dup net connection");
+       }
+
+       /* These will use binary mode on systems which have it.  */
+       /*
+        * Also, we know that from_server is shut down second, so we pass
+        * child_pid in there.  In theory, it should be stored in both
+        * buffers with a ref count...
+        */
+       *to_server_p = fd_buffer_initialize (tofd, 0, root, false, NULL);
+       *from_server_p = fd_buffer_initialize (fromfd, child_pid, root,
+                                               true, NULL);
+    }
+}
+#endif /* defined (AUTH_CLIENT_SUPPORT) || defined (SERVER_SUPPORT) || defined 
(HAVE_KERBEROS) || defined(HAVE_GSSAPI) */
+
+
+
+#if defined (AUTH_CLIENT_SUPPORT) || defined(HAVE_GSSAPI)
+/* Connect to the authenticating server.
+
+   If VERIFY_ONLY is non-zero, then just verify that the password is
+   correct and then shutdown the connection.
+
+   If VERIFY_ONLY is 0, then really connect to the server.
+
+   If DO_GSSAPI is non-zero, then we use GSSAPI authentication rather
+   than the pserver password authentication.
+
+   If we fail to connect or if access is denied, then die with fatal
+   error.  */
+void
+connect_to_pserver (cvsroot_t *root, struct buffer **to_server_p,
+                    struct buffer **from_server_p, int verify_only,
+                    int do_gssapi)
+{
+    int sock;
+    int port_number,
+       proxy_port_number = 0; /* Initialize to silence -Wall.  Dumb.  */
+    union sai {
+       struct sockaddr_in addr_in;
+       struct sockaddr addr;
+    } client_sai;
+    struct hostent *hostinfo;
+    struct buffer *to_server, *from_server;
+
+    sock = socket (AF_INET, SOCK_STREAM, 0);
+    if (sock == -1)
+       error (1, 0, "cannot create socket: %s", SOCK_STRERROR (SOCK_ERRNO));
+    port_number = get_cvs_port_number (root);
+
+    /* if we have a proxy connect to that instead */
+    if (root->proxy_hostname)
+    {
+        proxy_port_number = get_proxy_port_number (root);
+       hostinfo = init_sockaddr (&client_sai.addr_in, root->proxy_hostname,
+                                  proxy_port_number);
+        TRACE (TRACE_FUNCTION, "Connecting to %s:%d via proxy %s(%s):%d.",
+               root->hostname, port_number, root->proxy_hostname,
+               inet_ntoa (client_sai.addr_in.sin_addr), proxy_port_number);
+    }
+    else
+    {
+       hostinfo = init_sockaddr (&client_sai.addr_in, root->hostname,
+                                 port_number);
+        TRACE (TRACE_FUNCTION, "Connecting to %s(%s):%d.",
+               root->hostname,
+               inet_ntoa (client_sai.addr_in.sin_addr), port_number);
+    }
+
+    if (connect (sock, &client_sai.addr, sizeof (client_sai))
+       < 0)
+       error (1, 0, "connect to %s(%s):%d failed: %s",
+              root->proxy_hostname ? root->proxy_hostname : root->hostname,
+              inet_ntoa (client_sai.addr_in.sin_addr),
+              root->proxy_hostname ? proxy_port_number : port_number,
+               SOCK_STRERROR (SOCK_ERRNO));
+
+    make_bufs_from_fds (sock, sock, 0, root, &to_server, &from_server, 1);
+
+    /* if we have proxy then connect to the proxy first */
+    if (root->proxy_hostname)
+    {
+#define CONNECT_STRING "CONNECT %s:%d HTTP/1.0\r\n\r\n"
+       /* Send a "CONNECT" command to proxy: */
+       char* read_buf;
+       int codenum;
+       size_t count;
+       /* 4 characters for port covered by the length of %s & %d */
+       char* write_buf = Xasnprintf (NULL, &count, CONNECT_STRING,
+                                      root->hostname, port_number);
+       send_to_server_via (to_server, write_buf, count);
+
+       /* Wait for HTTP status code, bail out if you don't get back a 2xx
+         * code.
+         */
+       read_line_via (from_server, to_server, &read_buf);
+       sscanf (read_buf, "%s %d", write_buf, &codenum);
+
+       if ((codenum / 100) != 2)
+           error (1, 0, "proxy server %s:%d does not support http tunnelling",
+                  root->proxy_hostname, proxy_port_number);
+       free (read_buf);
+       free (write_buf);
+
+       /* Skip through remaining part of MIME header, recv_line
+           consumes the trailing \n */
+       while (read_line_via (from_server, to_server, &read_buf) > 0)
+       {
+           if (read_buf[0] == '\r' || read_buf[0] == 0)
+           {
+               free (read_buf);
+               break;
+           }
+           free (read_buf);
+       }
+    }
+
+    auth_server (root, to_server, from_server, verify_only, do_gssapi,
+                 hostinfo);
+
+    if (verify_only)
+    {
+       int status;
+
+       status = buf_shutdown (to_server);
+       if (status != 0)
+           error (0, status, "shutting down buffer to server");
+       buf_free (to_server);
+       to_server = NULL;
+
+       status = buf_shutdown (from_server);
+       if (status != 0)
+           error (0, status, "shutting down buffer from server");
+       buf_free (from_server);
+       from_server = NULL;
+
+       /* Don't need to set server_started = 0 since we don't set it to 1
+        * until returning from this call.
+        */
+    }
+    else
+    {
+       *to_server_p = to_server;
+       *from_server_p = from_server;
+    }
+
+    return;
+}
+
+
+
+static void
+auth_server (cvsroot_t *root, struct buffer *to_server,
+             struct buffer *from_server, int verify_only, int do_gssapi,
+             struct hostent *hostinfo)
+{
+    char *username = NULL;             /* the username we use to connect */
+    char no_passwd = 0;                        /* gets set if no password 
found */
+
+    /* Run the authorization mini-protocol before anything else. */
+    if (do_gssapi)
+    {
+# ifdef HAVE_GSSAPI
+       int fd = buf_get_fd (to_server);
+       struct stat s;
+
+       if ((fd < 0) || (fstat (fd, &s) < 0) || !S_ISSOCK(s.st_mode))
+       {
+           error (1, 0,
+                   "gserver currently only enabled for socket connections");
+       }
+
+       if (! connect_to_gserver (root, fd, hostinfo))
+       {
+           error (1, 0,
+                   "authorization failed: server %s rejected access to %s",
+                   root->hostname, root->directory);
+       }
+# else /* ! HAVE_GSSAPI */
+       error (1, 0,
+"INTERNAL ERROR: This client does not support GSSAPI authentication");
+# endif /* HAVE_GSSAPI */
+    }
+    else /* ! do_gssapi */
+    {
+# ifdef AUTH_CLIENT_SUPPORT
+       char *begin      = NULL;
+       char *password   = NULL;
+       char *end        = NULL;
+       
+       if (verify_only)
+       {
+           begin = "BEGIN VERIFICATION REQUEST";
+           end   = "END VERIFICATION REQUEST";
+       }
+       else
+       {
+           begin = "BEGIN AUTH REQUEST";
+           end   = "END AUTH REQUEST";
+       }
+
+       /* Get the password, probably from ~/.cvspass. */
+       password = get_cvs_password ();
+       username = root->username ? root->username : getcaller();
+
+       /* Send the empty string by default.  This is so anonymous CVS
+          access doesn't require client to have done "cvs login". */
+       if (!password) 
+       {
+           no_passwd = 1;
+           password = scramble ("");
+       }
+
+       /* Announce that we're starting the authorization protocol. */
+       send_to_server_via(to_server, begin, 0);
+       send_to_server_via(to_server, "\012", 1);
+
+       /* Send the data the server needs. */
+       send_to_server_via(to_server, root->directory, 0);
+       send_to_server_via(to_server, "\012", 1);
+       send_to_server_via(to_server, username, 0);
+       send_to_server_via(to_server, "\012", 1);
+       send_to_server_via(to_server, password, 0);
+       send_to_server_via(to_server, "\012", 1);
+
+       /* Announce that we're ending the authorization protocol. */
+       send_to_server_via(to_server, end, 0);
+       send_to_server_via(to_server, "\012", 1);
+
+        /* Paranoia. */
+        memset (password, 0, strlen (password));
+# else /* ! AUTH_CLIENT_SUPPORT */
+       error (1, 0, "INTERNAL ERROR: This client does not support pserver 
authentication");
+# endif /* AUTH_CLIENT_SUPPORT */
+    } /* if (do_gssapi) */
+
+    {
+       char *read_buf;
+
+       /* Loop, getting responses from the server.  */
+       while (1)
+       {
+           read_line_via (from_server, to_server, &read_buf);
+
+           if (!strcmp (read_buf, "I HATE YOU"))
+           {
+               /* Authorization not granted.
+                *
+                * This is a little confusing since we can reach this while
+                * loop in GSSAPI mode, but if GSSAPI authentication failed,
+                * we already jumped to the rejected label (there is no case
+                * where the connect_to_gserver function can return 1 and we
+                * will not receive "I LOVE YOU" from the server, barring
+                * broken connections and garbled messages, of course).  The
+                * GSSAPI case is also the case where username can be NULL
+                * since username is initialized in the !gssapi section.
+                *
+                * i.e. This is a pserver specific error message and should be
+                * since GSSAPI doesn't use username.
+                */
+               error (0, 0,
+"authorization failed: server %s rejected access to %s for user %s",
+                      root->hostname, root->directory,
+                      username ? username : "(null)");
+
+               /* Output a special error message if authentication was 
attempted
+               with no password -- the user should be made aware that they may
+               have missed a step. */
+               if (no_passwd)
+               {
+                   error (0, 0,
+"used empty password; try \"cvs login\" with a real password");
+               }
+               exit (EXIT_FAILURE);
+           }
+           else if (!strncmp (read_buf, "E ", 2))
+           {
+               fprintf (stderr, "%s\n", read_buf + 2);
+
+               /* Continue with the authentication protocol.  */
+           }
+           else if (!strncmp (read_buf, "error ", 6))
+           {
+               char *p;
+
+               /* First skip the code.  */
+               p = read_buf + 6;
+               while (*p != ' ' && *p != '\0')
+                   ++p;
+
+               /* Skip the space that follows the code.  */
+               if (*p == ' ')
+                   ++p;
+
+               /* Now output the text.  */
+               fprintf (stderr, "%s\n", p);
+               exit (EXIT_FAILURE);
+           }
+           else if (!strcmp (read_buf, "I LOVE YOU"))
+           {
+               free (read_buf);
+               break;
+           }
+           else
+           {
+               error (1, 0, 
+                      "unrecognized auth response from %s: %s", 
+                      root->hostname, read_buf);
+           }
+           free (read_buf);
+       }
+    }
+}
+#endif /* defined (AUTH_CLIENT_SUPPORT) || defined(HAVE_GSSAPI) */
+
+
+
+#if defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT)
+/* 
+ * Connect to a forked server process.
+ */
+static void
+connect_to_forked_server (cvsroot_t *root, struct buffer **to_server_p,
+                          struct buffer **from_server_p)
+{
+    int tofd, fromfd;
+    int child_pid;
+
+    /* This is pretty simple.  All we need to do is choose the correct
+       cvs binary and call piped_child. */
+
+     char *command[3];
+
+    command[0] = (root->cvs_server
+                 ? root->cvs_server : getenv ("CVS_SERVER"));
+    if (!command[0])
+# ifdef SERVER_SUPPORT
+        /* FIXME:
+         * I'm casting out the const below because I know that piped_child, the
+         * only function we pass COMMAND to, accepts COMMAND as a
+         * (char *const *) and won't alter it, and we don't alter it in this
+         * function.  This is yucky, there should be a way to declare COMMAND
+         * such that this casting isn't needed, but I don't know how.  If I
+         * declare it as (const char *command[]), the compiler complains about
+         * an incompatible arg 1 being passed to piped_child and if I declare
+         * it as (char *const command[3]), then the compiler complains when I
+         * assign values to command[i].
+         */
+       command[0] = (char *)program_path;
+# else /* SERVER_SUPPORT */
+    {
+       error( 0, 0, "You must set the CVS_SERVER environment variable when" );
+       error( 0, 0, "using the :fork: access method." );
+       error( 1, 0, "This CVS was not compiled with server support." );
+    }
+# endif /* SERVER_SUPPORT */
+    
+    command[1] = "server";
+    command[2] = NULL;
+
+    TRACE (TRACE_FUNCTION, "Forking server: %s %s",
+          command[0] ? command[0] : "(null)", command[1]);
+
+    child_pid = piped_child (command, &tofd, &fromfd, false);
+    if (child_pid < 0)
+       error (1, 0, "could not fork server process");
+
+    make_bufs_from_fds (tofd, fromfd, child_pid, root, to_server_p,
+                        from_server_p, 0);
+}
+#endif /* CLIENT_SUPPORT || SERVER_SUPPORT */
+
+
+
+static int
+send_variable_proc (Node *node, void *closure)
+{
+    send_to_server ("Set ", 0);
+    send_to_server (node->key, 0);
+    send_to_server ("=", 1);
+    send_to_server (node->data, 0);
+    send_to_server ("\012", 1);
+    return 0;
+}
+
+
+
+/* Open up the connection to the server and perform any necessary
+ * authentication.
+ */
+void
+open_connection_to_server (cvsroot_t *root, struct buffer **to_server_p,
+                           struct buffer **from_server_p)
+{
+    /* Note that generally speaking we do *not* fall back to a different
+       way of connecting if the first one does not work.  This is slow
+       (*really* slow on a 14.4kbps link); the clean way to have a CVS
+       which supports several ways of connecting is with access methods.  */
+
+    TRACE (TRACE_FUNCTION, "open_connection_to_server (%s)", root->original);
+
+    switch (root->method)
+    {
+       case pserver_method:
+#ifdef AUTH_CLIENT_SUPPORT
+           /* Toss the return value.  It will die with an error message if
+            * anything goes wrong anyway.
+            */
+           connect_to_pserver (root, to_server_p, from_server_p, 0, 0);
+#else /* AUTH_CLIENT_SUPPORT */
+           error (0, 0, "CVSROOT is set for a pserver access method but your");
+           error (1, 0, "CVS executable doesn't support it.");
+#endif /* AUTH_CLIENT_SUPPORT */
+           break;
+
+       case kserver_method:
+#if HAVE_KERBEROS
+           start_kerberos4_server (root, to_server_p, 
+                                    from_server_p);
+#else /* !HAVE_KERBEROS */
+           error (0, 0,
+                  "CVSROOT is set for a kerberos access method but your");
+           error (1, 0, "CVS executable doesn't support it.");
+#endif /* HAVE_KERBEROS */
+           break;
+
+       case gserver_method:
+#ifdef HAVE_GSSAPI
+           /* GSSAPI authentication is handled by the pserver.  */
+           connect_to_pserver (root, to_server_p, from_server_p, 0, 1);
+#else /* !HAVE_GSSAPI */
+           error (0, 0, "CVSROOT is set for a GSSAPI access method but your");
+           error (1, 0, "CVS executable doesn't support it.");
+#endif /* HAVE_GSSAPI */
+           break;
+
+       case ext_method:
+#ifdef NO_EXT_METHOD
+           error (0, 0, ":ext: method not supported by this port of CVS");
+           error (1, 0, "try :server: instead");
+#else /* ! NO_EXT_METHOD */
+           start_rsh_server (root, to_server_p,
+                              from_server_p);
+#endif /* NO_EXT_METHOD */
+           break;
+
+       case server_method:
+#ifdef START_SERVER
+           {
+           int tofd, fromfd;
+           START_SERVER (&tofd, &fromfd, getcaller (),
+                         root->username,
+                          root->hostname,
+                         root->directory);
+# ifdef START_SERVER_RETURNS_SOCKET
+           make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p,
+                                from_server_p, 1);
+# else /* ! START_SERVER_RETURNS_SOCKET */
+           make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p,
+                                from_server_p, 0);
+# endif /* START_SERVER_RETURNS_SOCKET */
+           }
+#else /* ! START_SERVER */
+           /* FIXME: It should be possible to implement this portably,
+              like pserver, which would get rid of the duplicated code
+              in {vms,windows-NT,...}/startserver.c.  */
+           error (1, 0,
+"the :server: access method is not supported by this port of CVS");
+#endif /* START_SERVER */
+           break;
+
+        case fork_method:
+           connect_to_forked_server (root, to_server_p, from_server_p);
+           break;
+
+       default:
+           error (1, 0,
+                   "(start_server internal error): unknown access method");
+           break;
+    }
+
+    /* "Hi, I'm Darlene and I'll be your server tonight..." */
+    server_started = 1;
+}
+
+
+
+/* Contact the server.  */
+void
+start_server (void)
+{
+    bool rootless;
+    int status;
+    bool have_global;
+
+    do
+    {
+       /* Clear our static variables for this invocation. */
+       if (toplevel_repos)
+           free (toplevel_repos);
+       toplevel_repos = NULL;
+
+       open_connection_to_server (current_parsed_root, &global_to_server,
+                                  &global_from_server);
+       setup_logfiles ("CVS_CLIENT_LOG", &global_to_server,
+                       &global_from_server);
+
+       /* Clear static variables.  */
+       if (toplevel_repos)
+       {
+           free (toplevel_repos);
+           toplevel_repos = NULL;
+       }
+       if (last_repos)
+       {
+           free (last_repos);
+           last_repos = NULL;
+       }
+       if (last_update_dir)
+       {
+           free (last_update_dir);
+           last_update_dir = NULL;
+       }
+       stored_checksum_valid = 0;
+       if (stored_mode)
+       {
+           free (stored_mode);
+           stored_mode = NULL;
+       }
+
+       rootless = !strcmp (cvs_cmd_name, "init");
+       if (!rootless)
+       {
+           send_to_server ("Root ", 0);
+           send_to_server (current_parsed_root->directory, 0);
+           send_to_server ("\012", 1);
+       }
+
+       {
+           struct response *rs;
+           bool suppress_redirect = !current_parsed_root->redirect;
+
+           send_to_server ("Valid-responses", 0);
+
+           for (rs = responses; rs->name; ++rs)
+           {
+               if (suppress_redirect && !strcmp (rs->name, "Redirect"))
+                   continue;
+
+               send_to_server (" ", 0);
+               send_to_server (rs->name, 0);
+           }
+           send_to_server ("\012", 1);
+       }
+       send_to_server ("valid-requests\012", 0);
+
+       if (get_server_responses ())
+           exit (EXIT_FAILURE);
+
+       have_global = supported_request ("Global_option");
+
+       /* Encryption needs to come before compression.  Good encryption can
+        * render compression useless in the other direction.
+        */
+       if (cvsencrypt && !rootless)
+       {
+#ifdef ENCRYPTION
+           /* Turn on encryption before turning on compression.  We do
+            * not want to try to compress the encrypted stream.  Instead,
+            * we want to encrypt the compressed stream.  If we can't turn
+            * on encryption, bomb out; don't let the user think the data
+            * is being encrypted when it is not.
+            */
+#  ifdef HAVE_KERBEROS
+           if (current_parsed_root->method == kserver_method)
+           {
+               if (!supported_request ("Kerberos-encrypt"))
+                   error (1, 0, "This server does not support encryption");
+               send_to_server ("Kerberos-encrypt\012", 0);
+              initialize_kerberos4_encryption_buffers (&global_to_server,
+                                                       &global_from_server);
+           }
+           else
+#  endif /* HAVE_KERBEROS */
+#  ifdef HAVE_GSSAPI
+           if (current_parsed_root->method == gserver_method)
+           {
+               if (!supported_request ("Gssapi-encrypt"))
+                   error (1, 0, "This server does not support encryption");
+               send_to_server ("Gssapi-encrypt\012", 0);
+               initialize_gssapi_buffers (&global_to_server,
+                                          &global_from_server);
+               cvs_gssapi_encrypt = 1;
+           }
+           else
+#  endif /* HAVE_GSSAPI */
+               error (1, 0,
+"Encryption is only supported when using GSSAPI or Kerberos");
+#else /* ! ENCRYPTION */
+           error (1, 0, "This client does not support encryption");
+#endif /* ! ENCRYPTION */
+       }
+
+       /* Send this before compression to enable supression of the
+        * "Forcing compression level Z" messages.
+        */
+       if (quiet)
+       {
+           if (have_global)
+           {
+               send_to_server ("Global_option -q\012", 0);
+           }
+           else
+               error (1, 0,
+                      "This server does not support the global -q option.");
+       }
+       if (really_quiet)
+       {
+           if (have_global)
+           {
+               send_to_server ("Global_option -Q\012", 0);
+           }
+           else
+               error (1, 0,
+                      "This server does not support the global -Q option.");
+       }
+
+       /* Compression needs to come before any of the rooted requests to
+        * work with compression limits.
+        */
+       if (!rootless && (gzip_level || force_gzip))
+       {
+           if (supported_request ("Gzip-stream"))
+           {
+               char *gzip_level_buf = Xasprintf ("%d", gzip_level);
+               send_to_server ("Gzip-stream ", 0);
+               send_to_server (gzip_level_buf, 0);
+               free (gzip_level_buf);
+               send_to_server ("\012", 1);
+
+               /* All further communication with the server will be
+                  compressed.  */
+
+               global_to_server =
+                   compress_buffer_initialize (global_to_server, 0,
+                                               gzip_level, NULL);
+               global_from_server =
+                   compress_buffer_initialize (global_from_server, 1,
+                                               gzip_level, NULL);
+           }
+#ifndef NO_CLIENT_GZIP_PROCESS
+           else if (supported_request ("gzip-file-contents"))
+           {
+               char *gzip_level_buf = Xasprintf ("%d", gzip_level);
+               send_to_server ("gzip-file-contents ", 0);
+               send_to_server (gzip_level_buf, 0);
+               free (gzip_level_buf);
+               send_to_server ("\012", 1);
+
+               file_gzip_level = gzip_level;
+           }
+#endif
+           else
+           {
+               fprintf (stderr, "server doesn't support gzip-file-contents\n");
+               /* Setting gzip_level to 0 prevents us from giving the
+                  error twice if update has to contact the server again
+                  to fetch unpatchable files.  */
+               gzip_level = 0;
+           }
+       }
+
+       if (client_referrer && supported_request ("Referrer"))
+       {
+           send_to_server ("Referrer ", 0);
+           send_to_server (client_referrer->original, 0);
+           send_to_server ("\012", 0);
+       }
+
+       /* FIXME: I think we should still be sending this for init.  */
+       if (!rootless && supported_request ("Command-prep"))
+       {
+           send_to_server ("Command-prep ", 0);
+           send_to_server (cvs_cmd_name, 0);
+           send_to_server ("\012", 0);
+           status = get_server_responses ();
+           if (status == 1) exit (EXIT_FAILURE);
+           if (status == 2) close_connection_to_server (&global_to_server,
+                                                        &global_from_server);
+       }
+       else status = 0;
+    } while (status == 2);
+
+
+    /*
+     * Now handle global options.
+     *
+     * -H, -f, -d, -e should be handled OK locally.
+     *
+     * -b we ignore (treating it as a server installation issue).
+     * FIXME: should be an error message.
+     *
+     * -v we print local version info; FIXME: Add a protocol request to get
+     * the version from the server so we can print that too.
+     *
+     * -l -t -r -w -q -n and -Q need to go to the server.
+     */
+    if (noexec)
+    {
+       if (have_global)
+       {
+           send_to_server ("Global_option -n\012", 0);
+       }
+       else
+           error (1, 0,
+                  "This server does not support the global -n option.");
+    }
+    if (!cvswrite)
+    {
+       if (have_global)
+       {
+           send_to_server ("Global_option -r\012", 0);
+       }
+       else
+           error (1, 0,
+                  "This server does not support the global -r option.");
+    }
+    if (trace)
+    {
+       if (have_global)
+       {
+           int count = trace;
+           while (count--) send_to_server ("Global_option -t\012", 0);
+       }
+       else
+           error (1, 0,
+                  "This server does not support the global -t option.");
+    }
+
+    /* Find out about server-side cvswrappers.  An extra network
+       turnaround for cvs import seems to be unavoidable, unless we
+       want to add some kind of client-side place to configure which
+       filenames imply binary.  For cvs add, we could avoid the
+       problem by keeping a copy of the wrappers in CVSADM (the main
+       reason to bother would be so we could make add work without
+       contacting the server, I suspect).  */
+
+    if (!strcmp (cvs_cmd_name, "import") || !strcmp (cvs_cmd_name, "add"))
+    {
+       if (supported_request ("wrapper-sendme-rcsOptions"))
+       {
+           int err;
+           send_to_server ("wrapper-sendme-rcsOptions\012", 0);
+           err = get_server_responses ();
+           if (err != 0)
+               error (err, 0, "error reading from server");
+       }
+    }
+
+    if (cvsauthenticate && ! cvsencrypt && !rootless)
+    {
+       /* Turn on authentication after turning on compression, so
+          that we can compress the authentication information.  We
+          assume that encrypted data is always authenticated--the
+          ability to decrypt the data stream is itself a form of
+          authentication.  */
+#ifdef HAVE_GSSAPI
+       if (current_parsed_root->method == gserver_method)
+       {
+           if (! supported_request ("Gssapi-authenticate"))
+               error (1, 0,
+                      "This server does not support stream authentication");
+           send_to_server ("Gssapi-authenticate\012", 0);
+           initialize_gssapi_buffers(&global_to_server, &global_from_server);
+
+       }
+       else
+           error (1, 0, "Stream authentication is only supported when using 
GSSAPI");
+#else /* ! HAVE_GSSAPI */
+       error (1, 0, "This client does not support stream authentication");
+#endif /* ! HAVE_GSSAPI */
+    }
+
+    /* If "Set" is not supported, just silently fail to send the variables.
+       Users with an old server should get a useful error message when it
+       fails to recognize the ${=foo} syntax.  This way if someone uses
+       several servers, some of which are new and some old, they can still
+       set user variables in their .cvsrc without trouble.  */
+    if (supported_request ("Set"))
+       walklist (variable_list, send_variable_proc, NULL);
+}
+
+
+
+/* Send an argument STRING.  */
+void
+send_arg (const char *string)
+{
+    const char *p = string;
+
+    send_to_server ("Argument ", 0);
+
+    while (*p)
+    {
+       if (*p == '\n')
+           send_to_server ("\012Argumentx ", 0);
+       else
+           send_to_server (p, 1);
+       ++p;
+    }
+    send_to_server ("\012", 1);
+}
+
+
+
+/* VERS->OPTIONS specifies whether the file is binary or not.  NOTE: BEFORE
+   using any other fields of the struct vers, we would need to fix
+   client_process_import_file to set them up.  */
+static void
+send_modified (const char *file, const char *short_pathname, Vers_TS *vers)
+{
+    /* File was modified, send it.  */
+    struct stat sb;
+    int fd;
+    unsigned char *buf;
+    char *mode_string;
+    size_t bufsize;
+    int bin;
+
+    TRACE (TRACE_FUNCTION, "Sending file `%s' to server", file);
+
+    /* Don't think we can assume fstat exists.  */
+    if (stat (file, &sb) < 0)
+       error (1, errno, "reading %s", short_pathname);
+
+    mode_string = mode_to_string (sb.st_mode);
+
+    /* Beware: on systems using CRLF line termination conventions,
+       the read and write functions will convert CRLF to LF, so the
+       number of characters read is not the same as sb.st_size.  Text
+       files should always be transmitted using the LF convention, so
+       we don't want to disable this conversion.  */
+    bufsize = sb.st_size;
+    buf = xmalloc (bufsize);
+
+    /* Is the file marked as containing binary data by the "-kb" flag?
+       If so, make sure to open it in binary mode: */
+
+    if (vers && vers->options)
+      bin = !strcmp (vers->options, "-kb");
+    else
+      bin = 0;
+
+#ifdef BROKEN_READWRITE_CONVERSION
+    if (!bin)
+    {
+       /* If only stdio, not open/write/etc., do text/binary
+          conversion, use convert_file which can compensate
+          (FIXME: we could just use stdio instead which would
+          avoid the whole problem).  */
+       char *tfile = Xasprintf ("%s.CVSBFCTMP", file);
+       convert_file (file, O_RDONLY,
+                     tfile, O_WRONLY | O_CREAT | O_TRUNC | OPEN_BINARY);
+       fd = CVS_OPEN (tfile, O_RDONLY | OPEN_BINARY);
+       if (fd < 0)
+           error (1, errno, "reading %s", short_pathname);
+       free (tfile);
+    }
+    else
+       fd = CVS_OPEN (file, O_RDONLY | OPEN_BINARY);
+#else
+    fd = CVS_OPEN (file, O_RDONLY | (bin ? OPEN_BINARY : 0));
+#endif
+
+    if (fd < 0)
+       error (1, errno, "reading %s", short_pathname);
+
+    if (file_gzip_level && sb.st_size > 100)
+    {
+       size_t newsize = 0;
+
+       if (read_and_gzip (fd, short_pathname, &buf,
+                          &bufsize, &newsize,
+                          file_gzip_level))
+           error (1, 0, "aborting due to compression error");
+
+       if (close (fd) < 0)
+           error (0, errno, "warning: can't close %s", short_pathname);
+
+        {
+          char tmp[80];
+
+         send_to_server ("Modified ", 0);
+         send_to_server (file, 0);
+         send_to_server ("\012", 1);
+         send_to_server (mode_string, 0);
+         send_to_server ("\012z", 2);
+         sprintf (tmp, "%lu\n", (unsigned long) newsize);
+         send_to_server (tmp, 0);
+
+          send_to_server (buf, newsize);
+        }
+    }
+    else
+    {
+       int newsize;
+
+        {
+           unsigned char *bufp = buf;
+           int len;
+
+           /* FIXME: This is gross.  It assumes that we might read
+              less than st_size bytes (true on NT), but not more.
+              Instead of this we should just be reading a block of
+              data (e.g. 8192 bytes), writing it to the network, and
+              so on until EOF.  */
+           while ((len = read (fd, bufp, (buf + sb.st_size) - bufp)) > 0)
+               bufp += len;
+
+           if (len < 0)
+               error (1, errno, "reading %s", short_pathname);
+
+           newsize = bufp - buf;
+       }
+       if (close (fd) < 0)
+           error (0, errno, "warning: can't close %s", short_pathname);
+
+        {
+          char tmp[80];
+
+         send_to_server ("Modified ", 0);
+         send_to_server (file, 0);
+         send_to_server ("\012", 1);
+         send_to_server (mode_string, 0);
+         send_to_server ("\012", 1);
+          sprintf (tmp, "%lu\012", (unsigned long) newsize);
+          send_to_server (tmp, 0);
+        }
+#ifdef BROKEN_READWRITE_CONVERSION
+       if (!bin)
+       {
+           char *tfile = Xasprintf ("%s.CVSBFCTMP", file);
+           if (CVS_UNLINK (tfile) < 0)
+               error (0, errno, "warning: can't remove temp file %s", tfile);
+           free (tfile);
+       }
+#endif
+
+       /*
+        * Note that this only ends with a newline if the file ended with
+        * one.
+        */
+       if (newsize > 0)
+           send_to_server (buf, newsize);
+    }
+    free (buf);
+    free (mode_string);
+}
+
+
+
+static void
+send_signature (const char *srepos, const char *filename, bool bin)
+{
+    char *sigbuf;
+    size_t len;
+
+    sigbuf = gen_signature (srepos, filename, bin, &len);
+
+    send_to_server ("Signature\012", 0);
+    send_to_server (sigbuf, len);
+    free (sigbuf);
+}
+
+
+
+/* The address of an instance of this structure is passed to
+   send_fileproc, send_filesdoneproc, and send_direntproc, as the
+   callerdat parameter.  */
+struct send_data
+{
+    /* Each of the following flags are zero for clear or nonzero for set.  */
+    bool build_dirs;
+    bool force;
+    bool no_contents;
+    bool backup_modified;
+    bool sign;
+};
+
+/* Deal with one file.  */
+static int
+send_fileproc (void *callerdat, struct file_info *finfo)
+{
+    struct send_data *args = callerdat;
+    Vers_TS *vers;
+    struct file_info xfinfo;
+    /* File name to actually use.  Might differ in case from
+       finfo->file.  */
+    const char *filename;
+
+    send_a_repository ("", finfo->repository, finfo->update_dir);
+
+    xfinfo = *finfo;
+    xfinfo.repository = NULL;
+    xfinfo.rcs = NULL;
+    vers = Version_TS (&xfinfo, NULL, NULL, NULL, 0, 0);
+
+    if (vers->entdata)
+       filename = vers->entdata->user;
+    else
+       filename = finfo->file;
+
+    if (vers->vn_user)
+    {
+       /* The Entries request.  */
+       send_to_server ("Entry /", 0);
+       send_to_server (filename, 0);
+       send_to_server ("/", 0);
+       send_to_server (vers->vn_user, 0);
+       send_to_server ("/", 0);
+       if (vers->ts_conflict)
+       {
+           if (vers->ts_user && !strcmp (vers->ts_conflict, vers->ts_user))
+               send_to_server ("+=", 0);
+           else
+               send_to_server ("+modified", 0);
+       }
+       send_to_server ("/", 0);
+       send_to_server (vers->entdata ? vers->entdata->options : vers->options,
+                       0);
+       send_to_server ("/", 0);
+       if (vers->entdata && vers->entdata->tag)
+       {
+           send_to_server ("T", 0);
+           send_to_server (vers->entdata->tag, 0);
+       }
+       else if (vers->entdata && vers->entdata->date)
+          {
+           send_to_server ("D", 0);
+           send_to_server (vers->entdata->date, 0);
+          }
+       send_to_server ("\012", 1);
+    }
+    else
+    {
+       /* It seems a little silly to re-read this on each file, but
+          send_dirent_proc doesn't get called if filenames are specified
+          explicitly on the command line.  */
+       wrap_add_file (CVSDOTWRAPPER, 1);
+
+       if (wrap_name_has (filename, WRAP_RCSOPTION))
+       {
+           /* No "Entry", but the wrappers did give us a kopt so we better
+              send it with "Kopt".  As far as I know this only happens
+              for "cvs add".  Question: is there any reason why checking
+              for options from wrappers isn't done in Version_TS?
+
+              Note: it might have been better to just remember all the
+              kopts on the client side, rather than send them to the server,
+              and have it send us back the same kopts.  But that seemed like
+              a bigger change than I had in mind making now.  */
+
+           if (supported_request ("Kopt"))
+           {
+               char *opt;
+
+               send_to_server ("Kopt ", 0);
+               opt = wrap_rcsoption (filename, 1);
+               send_to_server (opt, 0);
+               send_to_server ("\012", 1);
+               free (opt);
+           }
+           else
+               error (0, 0, "\
+warning: ignoring -k options due to server limitations");
+       }
+    }
+
+    if (!vers->ts_user)
+    {
+       /*
+        * Do we want to print "file was lost" like normal CVS?
+        * Would it always be appropriate?
+        */
+       /* File no longer exists.  Don't do anything, missing files
+          just happen.  */
+    }
+    else if (!vers->ts_rcs || args->force
+            || strcmp (vers->ts_conflict
+                       ? vers->ts_conflict : vers->ts_rcs, vers->ts_user)
+            || (vers->ts_conflict && !strcmp (cvs_cmd_name, "diff")))
+    {
+       if (args->no_contents
+           && supported_request ("Is-modified"))
+       {
+           send_to_server ("Is-modified ", 0);
+           send_to_server (filename, 0);
+           send_to_server ("\012", 1);
+       }
+       else
+       {
+           if (args->sign
+               && get_sign_commits (false, supported_request ("Signature")))
+           {
+               if (!supported_request ("Signature"))
+                   error (1, 0, "Server doesn't support commit signatures.");
+
+               send_signature (Short_Repository (finfo->repository),
+                               finfo->file,
+                               vers && !strcmp (vers->options, "-kb"));
+           }
+           send_modified (filename, finfo->fullname, vers);
+       }
+
+        if (args->backup_modified)
+        {
+            char *bakname;
+            bakname = backup_file (filename, vers->vn_user);
+            /* This behavior is sufficiently unexpected to
+               justify overinformativeness, I think. */
+            if (! really_quiet)
+                printf ("(Locally modified %s moved to %s)\n",
+                        filename, bakname);
+            free (bakname);
+        }
+    }
+    else
+    {
+       send_to_server ("Unchanged ", 0);
+       send_to_server (filename, 0);
+       send_to_server ("\012", 1);
+    }
+
+    /* if this directory has an ignore list, add this file to it */
+    if (ignlist)
+    {
+       Node *p;
+
+       p = getnode ();
+       p->type = FILES;
+       p->key = xstrdup (finfo->file);
+       (void) addnode (ignlist, p);
+    }
+
+    freevers_ts (&vers);
+    return 0;
+}
+
+
+
+static void
+send_ignproc (const char *file, const char *dir)
+{
+    if (ign_inhibit_server || !supported_request ("Questionable"))
+    {
+       if (dir[0] != '\0')
+           (void) printf ("? %s/%s\n", dir, file);
+       else
+           (void) printf ("? %s\n", file);
+    }
+    else
+    {
+       send_to_server ("Questionable ", 0);
+       send_to_server (file, 0);
+       send_to_server ("\012", 1);
+    }
+}
+
+
+
+static int
+send_filesdoneproc (void *callerdat, int err, const char *repository,
+                    const char *update_dir, List *entries)
+{
+    /* if this directory has an ignore list, process it then free it */
+    if (ignlist)
+    {
+       ignore_files (ignlist, entries, update_dir, send_ignproc);
+       dellist (&ignlist);
+    }
+
+    return err;
+}
+
+
+
+/*
+ * send_dirent_proc () is called back by the recursion processor before a
+ * sub-directory is processed for update.
+ * A return code of 0 indicates the directory should be
+ * processed by the recursion code.  A return of non-zero indicates the
+ * recursion code should skip this directory.
+ *
+ */
+static Dtype
+send_dirent_proc (void *callerdat, const char *dir, const char *repository,
+                  const char *update_dir, List *entries)
+{
+    struct send_data *args = callerdat;
+    int dir_exists;
+    char *cvsadm_name;
+
+    if (ignore_directory (update_dir))
+    {
+       /* print the warm fuzzy message */
+       if (!quiet)
+           error (0, 0, "Ignoring %s", update_dir);
+        return R_SKIP_ALL;
+    }
+
+    /*
+     * If the directory does not exist yet (e.g. "cvs update -d foo"),
+     * no need to send any files from it.  If the directory does not
+     * have a CVS directory, then we pretend that it does not exist.
+     * Otherwise, we will fail when trying to open the Entries file.
+     * This case will happen when checking out a module defined as
+     * ``-a .''.
+     */
+    cvsadm_name = Xasprintf ("%s/%s", dir, CVSADM);
+    dir_exists = isdir (cvsadm_name);
+    free (cvsadm_name);
+
+    /*
+     * If there is an empty directory (e.g. we are doing `cvs add' on a
+     * newly-created directory), the server still needs to know about it.
+     */
+
+    if (dir_exists)
+    {
+       /*
+        * Get the repository from a CVS/Repository file whenever possible.
+        * The repository variable is wrong if the names in the local
+        * directory don't match the names in the repository.
+        */
+       char *repos = Name_Repository (dir, update_dir);
+       send_a_repository (dir, repos, update_dir);
+       free (repos);
+
+       /* initialize the ignore list for this directory */
+       ignlist = getlist ();
+    }
+    else
+    {
+       /* It doesn't make sense to send a non-existent directory,
+          because there is no way to get the correct value for
+          the repository (I suppose maybe via the expand-modules
+          request).  In the case where the "obvious" choice for
+          repository is correct, the server can figure out whether
+          to recreate the directory; in the case where it is wrong
+          (that is, does not match what modules give us), we might as
+          well just fail to recreate it.
+
+          Checking for noexec is a kludge for "cvs -n add dir".  */
+       /* Don't send a non-existent directory unless we are building
+           new directories (build_dirs is true).  Otherwise, CVS may
+           see a D line in an Entries file, and recreate a directory
+           which the user removed by hand.  */
+       if (args->build_dirs && noexec)
+           send_a_repository (dir, repository, update_dir);
+    }
+
+    return dir_exists ? R_PROCESS : R_SKIP_ALL;
+}
+
+
+
+/*
+ * send_dirleave_proc () is called back by the recursion code upon leaving
+ * a directory.  All it does is delete the ignore list if it hasn't already
+ * been done (by send_filesdone_proc).
+ */
+/* ARGSUSED */
+static int
+send_dirleave_proc (void *callerdat, const char *dir, int err,
+                    const char *update_dir, List *entries )
+{
+
+    /* Delete the ignore list if it hasn't already been done.  */
+    if (ignlist)
+       dellist (&ignlist);
+    return err;
+}
+
+
+
+/*
+ * Send each option in an array to the server, one by one.
+ * argv might be "--foo=bar",  "-C", "5", "-y".
+ */
+
+void
+send_options (int argc, char * const *argv)
+{
+    int i;
+    for (i = 0; i < argc; i++)
+       send_arg (argv[i]);
+}
+
+
+
+/* Send the names of all the argument files to the server.  */
+void
+send_file_names (int argc, char **argv, unsigned int flags)
+{
+    int i;
+    
+    /* The fact that we do this here as well as start_recursion is a bit 
+       of a performance hit.  Perhaps worth cleaning up someday.  */
+    if (flags & SEND_EXPAND_WILD)
+       expand_wild (argc, argv, &argc, &argv);
+
+    for (i = 0; i < argc; ++i)
+    {
+       char buf[1];
+       char *p;
+#ifdef FILENAMES_CASE_INSENSITIVE
+       char *line = NULL;
+#endif /* FILENAMES_CASE_INSENSITIVE */
+
+       if (arg_should_not_be_sent_to_server (argv[i]))
+           continue;
+
+#ifdef FILENAMES_CASE_INSENSITIVE
+       /* We want to send the path as it appears in the
+          CVS/Entries files.  We put this inside an ifdef
+          to avoid doing all these system calls in
+          cases where fncmp is just strcmp anyway.  */
+       /* The isdir (CVSADM) check could more gracefully be replaced
+          with a way of having Entries_Open report back the
+          error to us and letting us ignore existence_error.
+          Or some such.  */
+       {
+           List *stack;
+           size_t line_len = 0;
+           char *q, *r;
+           struct saved_cwd sdir;
+
+           /* Split the argument onto the stack.  */
+           stack = getlist();
+           r = xstrdup (argv[i]);
+            /* It's okay to discard the const from the last_component return
+             * below since we know we passed in an arg that was not const.
+             */
+           while ((q = (char *)last_component (r)) != r)
+           {
+               push (stack, xstrdup (q));
+               *--q = '\0';
+           }
+           push (stack, r);
+
+           /* Normalize the path into outstr. */
+           save_cwd (&sdir);
+           while (q = pop (stack))
+           {
+               Node *node = NULL;
+               if (isdir (CVSADM))
+               {
+                   List *entries;
+
+                   /* Note that if we are adding a directory,
+                      the following will read the entry
+                      that we just wrote there, that is, we
+                      will get the case specified on the
+                      command line, not the case of the
+                      directory in the filesystem.  This
+                      is correct behavior.  */
+                   entries = Entries_Open (0, NULL);
+                   node = findnode_fn (entries, q);
+                   if (node)
+                   {
+                       /* Add the slash unless this is our first element. */
+                       if (line_len)
+                           xrealloc_and_strcat (&line, &line_len, "/");
+                       xrealloc_and_strcat (&line, &line_len, node->key);
+                       delnode (node);
+                   }
+                   Entries_Close (entries);
+               }
+
+               /* If node is still NULL then we either didn't find CVSADM or
+                * we didn't find an entry there.
+                */
+               if (!node)
+               {
+                   /* Add the slash unless this is our first element. */
+                   if (line_len)
+                       xrealloc_and_strcat (&line, &line_len, "/");
+                   xrealloc_and_strcat (&line, &line_len, q);
+                   break;
+               }
+
+               /* And descend the tree. */
+               if (isdir (q))
+                   CVS_CHDIR (q);
+               free (q);
+           }
+           restore_cwd (&sdir);
+           free_cwd (&sdir);
+
+           /* Now put everything we didn't find entries for back on. */
+           while (q = pop (stack))
+           {
+               if (line_len)
+                   xrealloc_and_strcat (&line, &line_len, "/");
+               xrealloc_and_strcat (&line, &line_len, q);
+               free (q);
+           }
+
+           p = line;
+
+           dellist (&stack);
+       }
+#else /* !FILENAMES_CASE_INSENSITIVE */
+       p = argv[i];
+#endif /* FILENAMES_CASE_INSENSITIVE */
+
+       send_to_server ("Argument ", 0);
+
+       while (*p)
+       {
+           if (*p == '\n')
+           {
+               send_to_server ("\012Argumentx ", 0);
+           }
+           else if (ISSLASH (*p))
+           {
+               buf[0] = '/';
+               send_to_server (buf, 1);
+           }
+           else
+           {
+               buf[0] = *p;
+               send_to_server (buf, 1);
+           }
+           ++p;
+       }
+       send_to_server ("\012", 1);
+#ifdef FILENAMES_CASE_INSENSITIVE
+       free (line);
+#endif /* FILENAMES_CASE_INSENSITIVE */
+    }
+
+    if (flags & SEND_EXPAND_WILD)
+    {
+       int i;
+       for (i = 0; i < argc; ++i)
+           free (argv[i]);
+       free (argv);
+    }
+}
+
+
+
+/* Calculate and send max-dotdot to the server */
+static void
+send_max_dotdot (argc, argv)
+    int argc;
+    char **argv;
+{
+    int i;
+    int level = 0;
+    int max_level = 0;
+
+    /* Send Max-dotdot if needed.  */
+    for (i = 0; i < argc; ++i)
+    {
+        level = pathname_levels (argv[i]);
+       if (level > 0)
+       {
+            if (!uppaths) uppaths = getlist();
+           push_string (uppaths, xstrdup (argv[i]));
+       }
+        if (level > max_level)
+            max_level = level;
+    }
+
+    if (max_level > 0)
+    {
+        if (supported_request ("Max-dotdot"))
+        {
+            char buf[10];
+            sprintf (buf, "%d", max_level);
+
+            send_to_server ("Max-dotdot ", 0);
+            send_to_server (buf, 0);
+            send_to_server ("\012", 1);
+        }
+        else
+        {
+            error (1, 0,
+"backreference in path (`..') not supported by old (pre-Max-dotdot) servers");
+        }
+    }
+}
+
+
+
+/*
+ * Send Repository, Modified and Entry.  Also sends Argument lines for argc
+ * and argv, so should be called after options are sent.  
+ *
+ * ARGUMENTS
+ *   argc      # of files to operate on (0 for everything).
+ *   argv      Paths to file to operate on.
+ *   local     nonzero if we should not recurse (-l option).
+ *   flags     FLAGS & SEND_BUILD_DIRS if nonexistent directories should be
+ *             sent.
+ *             FLAGS & SEND_FORCE if we should send unmodified files to the
+ *             server as though they were modified.
+ *             FLAGS & SEND_NO_CONTENTS means that this command only needs to
+ *             know _whether_ a file is modified, not the contents.
+ *   sign      Whether to send files signatures.
+ *
+ * RETURNS
+ *   Nothing.
+ */
+void
+send_files (int argc, char **argv, int local, int aflag, unsigned int flags)
+{
+    struct send_data args;
+    int err;
+
+    send_max_dotdot (argc, argv);
+
+    /*
+     * aflag controls whether the tag/date is copied into the vers_ts.
+     * But we don't actually use it, so I don't think it matters what we pass
+     * for aflag here.
+     */
+    args.build_dirs = flags & SEND_BUILD_DIRS;
+    args.force = flags & SEND_FORCE;
+    args.no_contents = flags & SEND_NO_CONTENTS;
+    args.backup_modified = flags & BACKUP_MODIFIED_FILES;
+    args.sign = flags & SEND_SIGNATURES;
+    err = start_recursion
+       (send_fileproc, send_filesdoneproc, send_dirent_proc,
+         send_dirleave_proc, &args, argc, argv, local, W_LOCAL, aflag,
+         CVS_LOCK_NONE, NULL, 0, NULL);
+    if (err)
+       exit (EXIT_FAILURE);
+    if (!toplevel_repos)
+       /*
+        * This happens if we are not processing any files,
+        * or for checkouts in directories without any existing stuff
+        * checked out.  The following assignment is correct for the
+        * latter case; I don't think toplevel_repos matters for the
+        * former.
+        */
+       toplevel_repos = xstrdup (current_parsed_root->directory);
+    send_repository ("", toplevel_repos, ".");
+}
+
+
+
+void
+client_import_setup (char *repository)
+{
+    if (!toplevel_repos)               /* should always be true */
+        send_a_repository ("", repository, "");
+}
+
+
+
+/*
+ * Process the argument import file.
+ */
+int
+client_process_import_file (char *message, char *vfile, char *vtag, int targc,
+                            char *targv[], char *repository,
+                            int all_files_binary,
+                            int modtime /* Nonzero for "import -d".  */ )
+{
+    char *update_dir;
+    char *fullname;
+    Vers_TS vers;
+
+    assert (toplevel_repos);
+
+    if (strncmp (repository, toplevel_repos, strlen (toplevel_repos)))
+       error (1, 0,
+              "internal error: pathname `%s' doesn't specify file in `%s'",
+              repository, toplevel_repos);
+
+    if (!strcmp (repository, toplevel_repos))
+    {
+       update_dir = "";
+       fullname = xstrdup (vfile);
+    }
+    else
+    {
+       update_dir = repository + strlen (toplevel_repos) + 1;
+
+       fullname = Xasprintf ("%s/%s", update_dir, vfile);
+    }
+
+    send_a_repository ("", repository, update_dir);
+    if (all_files_binary)
+       vers.options = xstrdup ("-kb");
+    else
+       vers.options = wrap_rcsoption (vfile, 1);
+
+    if (vers.options)
+    {
+       if (supported_request ("Kopt"))
+       {
+           send_to_server ("Kopt ", 0);
+           send_to_server (vers.options, 0);
+           send_to_server ("\012", 1);
+       }
+       else
+           error (0, 0,
+                  "warning: ignoring -k options due to server limitations");
+    }
+    if (modtime)
+    {
+       if (supported_request ("Checkin-time"))
+       {
+           struct stat sb;
+           char *rcsdate;
+           char netdate[MAXDATELEN];
+
+           if (stat (vfile, &sb) < 0)
+               error (1, errno, "cannot stat %s", fullname);
+           rcsdate = date_from_time_t (sb.st_mtime);
+           date_to_internet (netdate, rcsdate);
+           free (rcsdate);
+
+           send_to_server ("Checkin-time ", 0);
+           send_to_server (netdate, 0);
+           send_to_server ("\012", 1);
+       }
+       else
+           error (0, 0,
+                  "warning: ignoring -d option due to server limitations");
+    }
+
+    /* Send signature.  */
+    if (get_sign_commits (false, supported_request ("Signature")))
+    {
+       if (!supported_request ("Signature"))
+           error (1, 0, "Server doesn't support commit signatures.");
+
+       send_signature (Short_Repository (repository), vfile,
+                       vers.options && !strcmp (vers.options, "-kb"));
+    }
+
+    send_modified (vfile, fullname, &vers);
+    if (vers.options)
+       free (vers.options);
+    free (fullname);
+    return 0;
+}
+
+
+
+void
+client_import_done (void)
+{
+    if (!toplevel_repos)
+       /*
+        * This happens if we are not processing any files,
+        * or for checkouts in directories without any existing stuff
+        * checked out.  The following assignment is correct for the
+        * latter case; I don't think toplevel_repos matters for the
+        * former.
+        */
+        /* FIXME: "can't happen" now that we call client_import_setup
+          at the beginning.  */
+       toplevel_repos = xstrdup (current_parsed_root->directory);
+    send_repository ("", toplevel_repos, ".");
+}
+
+
+
+void
+client_notify (const char *repository, const char *update_dir,
+               const char *filename, int notif_type, const char *val)
+{
+    char buf[2];
+
+    send_a_repository ("", repository, update_dir);
+    send_to_server ("Notify ", 0);
+    send_to_server (filename, 0);
+    send_to_server ("\012", 1);
+    buf[0] = notif_type;
+    buf[1] = '\0';
+    send_to_server (buf, 1);
+    send_to_server ("\t", 1);
+    send_to_server (val, 0);
+}
+
+
+
+/*
+ * Send an option with an argument, dealing correctly with newlines in
+ * the argument.  If ARG is NULL, forget the whole thing.
+ */
+void
+option_with_arg (const char *option, const char *arg)
+{
+    if (!arg)
+       return;
+
+    send_to_server ("Argument ", 0);
+    send_to_server (option, 0);
+    send_to_server ("\012", 1);
+
+    send_arg (arg);
+}
+
+
+
+/* Send a date to the server.  The input DATE is in RCS format.
+   The time will be GMT.
+
+   We then convert that to the format required in the protocol
+   (including the "-D" option) and send it.  According to
+   cvsclient.texi, RFC 822/1123 format is preferred.  */
+void
+client_senddate (const char *date)
+{
+    char buf[MAXDATELEN];
+
+    date_to_internet (buf, date);
+    option_with_arg ("-D", buf);
+}
+
+
+
+void
+send_init_command (void)
+{
+    /* This is here because we need the current_parsed_root->directory 
variable.  */
+    send_to_server ("init ", 0);
+    send_to_server (current_parsed_root->directory, 0);
+    send_to_server ("\012", 0);
+}
+
+
+
+#if defined AUTH_CLIENT_SUPPORT || defined HAVE_KERBEROS || defined HAVE_GSSAPI
+
+struct hostent *
+init_sockaddr (struct sockaddr_in *name, char *hostname, unsigned int port)
+{
+    struct hostent *hostinfo;
+    unsigned short shortport = port;
+
+    memset (name, 0, sizeof (*name));
+    name->sin_family = AF_INET;
+    name->sin_port = htons (shortport);
+    hostinfo = gethostbyname (hostname);
+    if (!hostinfo)
+    {
+       fprintf (stderr, "Unknown host %s.\n", hostname);
+       exit (EXIT_FAILURE);
+    }
+    name->sin_addr = *(struct in_addr *) hostinfo->h_addr;
+    return hostinfo;
+}
+
+#endif /* defined AUTH_CLIENT_SUPPORT || defined HAVE_KERBEROS
+       * || defined HAVE_GSSAPI
+       */
+
+#endif /* CLIENT_SUPPORT */




reply via email to

[Prev in Thread] Current Thread [Next in Thread]