summaryrefslogtreecommitdiff
path: root/src/update.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/update.c')
-rw-r--r--src/update.c2907
1 files changed, 2907 insertions, 0 deletions
diff --git a/src/update.c b/src/update.c
new file mode 100644
index 0000000..4fb3440
--- /dev/null
+++ b/src/update.c
@@ -0,0 +1,2907 @@
+/*
+ * Copyright (C) 1986-2005 The Free Software Foundation, Inc.
+ *
+ * Portions Copyright (C) 1998-2005 Derek Price, Ximbiot <http://ximbiot.com>,
+ * and others.
+ *
+ * Portions Copyright (C) 1992, Brian Berliner and Jeff Polk
+ * Portions Copyright (C) 1989-1992, Brian Berliner
+ *
+ * You may distribute under the terms of the GNU General Public License as
+ * specified in the README file that comes with the CVS source distribution.
+ *
+ * "update" updates the version in the present directory with respect to the RCS
+ * repository. The present version must have been created by "checkout". The
+ * user can keep up-to-date by calling "update" whenever he feels like it.
+ *
+ * The present version can be committed by "commit", but this keeps the version
+ * in tact.
+ *
+ * Arguments following the options are taken to be file names to be updated,
+ * rather than updating the entire directory.
+ *
+ * Modified or non-existent RCS files are checked out and reported as U
+ * <user_file>
+ *
+ * Modified user files are reported as M <user_file>. If both the RCS file and
+ * the user file have been modified, the user file is replaced by the result
+ * of rcsmerge, and a backup file is written for the user in .#file.version.
+ * If this throws up irreconcilable differences, the file is reported as C
+ * <user_file>, and as M <user_file> otherwise.
+ *
+ * Files added but not yet committed are reported as A <user_file>. Files
+ * removed but not yet committed are reported as R <user_file>.
+ *
+ * If the current directory contains subdirectories that hold concurrent
+ * versions, these are updated too. If the -d option was specified, new
+ * directories added to the repository are automatically created and updated
+ * as well.
+ */
+
+#include "cvs.h"
+#include <assert.h>
+#include "save-cwd.h"
+#ifdef SERVER_SUPPORT
+# include "md5.h"
+#endif
+#include "watch.h"
+#include "fileattr.h"
+#include "edit.h"
+#include "getline.h"
+#include "buffer.h"
+#include "hardlink.h"
+
+static int checkout_file (struct file_info *finfo, Vers_TS *vers_ts,
+ int adding, int merging, int update_server);
+#ifdef SERVER_SUPPORT
+static void checkout_to_buffer (void *, const char *, size_t);
+static int patch_file (struct file_info *finfo,
+ Vers_TS *vers_ts,
+ int *docheckout, struct stat *file_info,
+ unsigned char *checksum);
+static void patch_file_write (void *, const char *, size_t);
+#endif
+static int merge_file (struct file_info *finfo, Vers_TS *vers);
+static int scratch_file (struct file_info *finfo, Vers_TS *vers);
+static Dtype update_dirent_proc (void *callerdat, const char *dir,
+ const char *repository,
+ const char *update_dir,
+ List *entries);
+static int update_dirleave_proc (void *callerdat, const char *dir,
+ int err, const char *update_dir,
+ List *entries);
+static int update_fileproc (void *callerdat, struct file_info *);
+static int update_filesdone_proc (void *callerdat, int err,
+ const char *repository,
+ const char *update_dir, List *entries);
+#ifdef PRESERVE_PERMISSIONS_SUPPORT
+static int get_linkinfo_proc( void *_callerdat, struct _finfo * );
+#endif
+static void join_file (struct file_info *finfo, Vers_TS *vers_ts);
+
+static char *options = NULL;
+static char *tag = NULL;
+static char *date = NULL;
+/* This is a bit of a kludge. We call WriteTag at the beginning
+ before we know whether nonbranch is set or not. And then at the
+ end, once we have the right value for nonbranch, we call WriteTag
+ again. I don't know whether the first call is necessary or not.
+ rewrite_tag is nonzero if we are going to have to make that second
+ call. warned is nonzero if we've already warned the user that the
+ tag occurs as both a revision tag and a branch tag. */
+static int rewrite_tag;
+static int nonbranch;
+static int warned;
+
+/* If we set the tag or date for a subdirectory, we use this to undo
+ the setting. See update_dirent_proc. */
+static char *tag_update_dir;
+
+static char *join_rev1, *join_date1;
+static char *join_rev2, *join_date2;
+static int aflag = 0;
+static int toss_local_changes = 0;
+static int force_tag_match = 1;
+static int update_build_dirs = 0;
+static int update_prune_dirs = 0;
+static int pipeout = 0;
+static int dotemplate = 0;
+#ifdef SERVER_SUPPORT
+static int patches = 0;
+static int rcs_diff_patches = 0;
+#endif
+static List *ignlist = NULL;
+static time_t last_register_time;
+static const char *const update_usage[] =
+{
+ "Usage: %s %s [-APCdflRp] [-k kopt] [-r rev] [-D date] [-j rev]\n",
+ " [-I ign] [-W spec] [files...]\n",
+ "\t-A\tReset any sticky tags/date/kopts.\n",
+ "\t-P\tPrune empty directories.\n",
+ "\t-C\tOverwrite locally modified files with clean repository copies.\n",
+ "\t-d\tBuild directories, like checkout does.\n",
+ "\t-f\tForce a head revision match if tag/date not found.\n",
+ "\t-l\tLocal directory only, no recursion.\n",
+ "\t-R\tProcess directories recursively.\n",
+ "\t-p\tSend updates to standard output (avoids stickiness).\n",
+ "\t-k kopt\tUse RCS kopt -k option on checkout. (is sticky)\n",
+ "\t-r rev\tUpdate using specified revision/tag (is sticky).\n",
+ "\t-D date\tSet date to update from (is sticky).\n",
+ "\t-j rev\tMerge in changes made between current revision and rev.\n",
+ "\t-I ign\tMore files to ignore (! to reset).\n",
+ "\t-W spec\tWrappers specification line.\n",
+ "(Specify the --help global option for a list of other help options)\n",
+ NULL
+};
+
+
+
+/*
+ * update is the argv,argc based front end for arg parsing
+ */
+int
+update (int argc, char **argv)
+{
+ int c, err;
+ int local = 0; /* recursive by default */
+ int which; /* where to look for files and dirs */
+ char *xjoin_rev1, *xjoin_date1,
+ *xjoin_rev2, *xjoin_date2,
+ *join_orig1, *join_orig2;
+
+ if (argc == -1)
+ usage (update_usage);
+
+ xjoin_rev1 = xjoin_date1 = xjoin_rev2 = xjoin_date2 = join_orig1 =
+ join_orig2 = NULL;
+
+ ign_setup ();
+ wrap_setup ();
+
+ /* parse the args */
+ optind = 0;
+ while ((c = getopt (argc, argv, "+ApCPflRQqduk:r:D:j:I:W:")) != -1)
+ {
+ switch (c)
+ {
+ case 'A':
+ aflag = 1;
+ break;
+ case 'C':
+ toss_local_changes = 1;
+ break;
+ case 'I':
+ ign_add (optarg, 0);
+ break;
+ case 'W':
+ wrap_add (optarg, 0);
+ break;
+ case 'k':
+ if (options)
+ free (options);
+ options = RCS_check_kflag (optarg);
+ break;
+ case 'l':
+ local = 1;
+ break;
+ case 'R':
+ local = 0;
+ break;
+ case 'Q':
+ case 'q':
+ /* The CVS 1.5 client sends these options (in addition to
+ Global_option requests), so we must ignore them. */
+ if (!server_active)
+ error (1, 0,
+ "-q or -Q must be specified before \"%s\"",
+ cvs_cmd_name);
+ break;
+ case 'd':
+ update_build_dirs = 1;
+ break;
+ case 'f':
+ force_tag_match = 0;
+ break;
+ case 'r':
+ parse_tagdate (&tag, &date, optarg);
+ break;
+ case 'D':
+ if (date) free (date);
+ date = Make_Date (optarg);
+ break;
+ case 'P':
+ update_prune_dirs = 1;
+ break;
+ case 'p':
+ pipeout = 1;
+ noexec = 1; /* so no locks will be created */
+ break;
+ case 'j':
+ if (join_orig2)
+ error (1, 0, "only two -j options can be specified");
+ if (join_orig1)
+ {
+ join_orig2 = xstrdup (optarg);
+ parse_tagdate (&xjoin_rev2, &xjoin_date2, optarg);
+ }
+ else
+ {
+ join_orig1 = xstrdup (optarg);
+ parse_tagdate (&xjoin_rev1, &xjoin_date1, optarg);
+ }
+ break;
+ case 'u':
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ {
+ patches = 1;
+ rcs_diff_patches = server_use_rcs_diff ();
+ }
+ else
+#endif
+ usage (update_usage);
+ break;
+ case '?':
+ default:
+ usage (update_usage);
+ break;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+#ifdef CLIENT_SUPPORT
+ if (current_parsed_root->isremote)
+ {
+ int pass;
+
+ /* The first pass does the regular update. If we receive at least
+ one patch which failed, we do a second pass and just fetch
+ those files whose patches failed. */
+ pass = 1;
+ do
+ {
+ int status;
+
+ start_server ();
+
+ if (local)
+ send_arg("-l");
+ if (update_build_dirs)
+ send_arg("-d");
+ if (pipeout)
+ send_arg("-p");
+ if (!force_tag_match)
+ send_arg("-f");
+ if (aflag)
+ send_arg("-A");
+ if (toss_local_changes)
+ send_arg("-C");
+ if (update_prune_dirs)
+ send_arg("-P");
+ client_prune_dirs = update_prune_dirs;
+ option_with_arg ("-r", tag);
+ if (options && options[0] != '\0')
+ send_arg (options);
+ if (date)
+ client_senddate (date);
+ if (join_orig1)
+ option_with_arg ("-j", join_orig1);
+ if (join_orig2)
+ option_with_arg ("-j", join_orig2);
+ wrap_send ();
+
+ if (failed_patches_count == 0)
+ {
+ unsigned int flags = 0;
+
+ /* If the server supports the command "update-patches", that
+ means that it knows how to handle the -u argument to update,
+ which means to send patches instead of complete files.
+
+ We don't send -u if failed_patches != NULL, so that the
+ server doesn't try to send patches which will just fail
+ again. At least currently, the client also clobbers the
+ file and tells the server it is lost, which also will get
+ a full file instead of a patch, but it seems clean to omit
+ -u. */
+ if (supported_request ("update-patches"))
+ send_arg ("-u");
+
+ send_arg ("--");
+
+ if (update_build_dirs)
+ flags |= SEND_BUILD_DIRS;
+
+ if (toss_local_changes) {
+ flags |= SEND_NO_CONTENTS;
+ flags |= BACKUP_MODIFIED_FILES;
+ }
+
+ /* If noexec, probably could be setting SEND_NO_CONTENTS.
+ Same caveats as for "cvs status" apply. */
+
+ send_files (argc, argv, local, aflag, flags);
+ send_file_names (argc, argv, SEND_EXPAND_WILD);
+ }
+ else
+ {
+ int i;
+
+ (void) printf ("%s client: refetching unpatchable files\n",
+ program_name);
+
+ if (toplevel_wd != NULL
+ && CVS_CHDIR (toplevel_wd) < 0)
+ {
+ error (1, errno, "could not chdir to %s", toplevel_wd);
+ }
+
+ send_arg ("--");
+
+ for (i = 0; i < failed_patches_count; i++)
+ if (unlink_file (failed_patches[i]) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s",
+ failed_patches[i]);
+ send_files (failed_patches_count, failed_patches, local,
+ aflag, update_build_dirs ? SEND_BUILD_DIRS : 0);
+ send_file_names (failed_patches_count, failed_patches, 0);
+ free_names (&failed_patches_count, failed_patches);
+ }
+
+ send_to_server ("update\012", 0);
+
+ status = get_responses_and_close ();
+
+ /* If there are any conflicts, the server will return a
+ non-zero exit status. If any patches failed, we still
+ want to run the update again. We use a pass count to
+ avoid an endless loop. */
+
+ /* Notes: (1) assuming that status != 0 implies a
+ potential conflict is the best we can cleanly do given
+ the current protocol. I suppose that trying to
+ re-fetch in cases where there was a more serious error
+ is probably more or less harmless, but it isn't really
+ ideal. (2) it would be nice to have a testsuite case for the
+ conflict-and-patch-failed case. */
+
+ if (status != 0
+ && (failed_patches_count == 0 || pass > 1))
+ {
+ if (failed_patches_count > 0)
+ free_names (&failed_patches_count, failed_patches);
+ return status;
+ }
+
+ ++pass;
+ } while (failed_patches_count > 0);
+
+ return 0;
+ }
+#endif
+
+ if (tag != NULL)
+ tag_check_valid (tag, argc, argv, local, aflag, "", false);
+ if (join_rev1 != NULL)
+ tag_check_valid (xjoin_rev1, argc, argv, local, aflag, "", false);
+ if (join_rev2 != NULL)
+ tag_check_valid (xjoin_rev2, argc, argv, local, aflag, "", false);
+
+ /*
+ * If we are updating the entire directory (for real) and building dirs
+ * as we go, we make sure there is no static entries file and write the
+ * tag file as appropriate
+ */
+ if (argc <= 0 && !pipeout)
+ {
+ if (update_build_dirs)
+ {
+ if (unlink_file (CVSADM_ENTSTAT) < 0 && ! existence_error (errno))
+ error (1, errno, "cannot remove file %s", CVSADM_ENTSTAT);
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ {
+ char *repos = Name_Repository (NULL, NULL);
+ server_clear_entstat (".", repos);
+ free (repos);
+ }
+#endif
+ }
+
+ /* keep the CVS/Tag file current with the specified arguments */
+ if (aflag || tag || date)
+ {
+ char *repos = Name_Repository (NULL, NULL);
+ WriteTag (NULL, tag, date, 0, ".", repos);
+ free (repos);
+ rewrite_tag = 1;
+ nonbranch = -1;
+ warned = 0;
+ }
+ }
+
+ /* look for files/dirs locally and in the repository */
+ which = W_LOCAL | W_REPOS;
+
+ /* look in the attic too if a tag or date is specified */
+ if (tag || date || join_orig1)
+ {
+ TRACE (TRACE_DATA, "update: searching attic");
+ which |= W_ATTIC;
+ }
+
+ /* call the command line interface */
+ err = do_update (argc, argv, options, tag, date, force_tag_match,
+ local, update_build_dirs, aflag, update_prune_dirs,
+ pipeout, which, xjoin_rev1, xjoin_date1, xjoin_rev2,
+ xjoin_date2, NULL, 1, NULL);
+
+ /* Free the space allocated for tags and dates, if necessary. */
+ if (tag) free (tag);
+ if (date) free (date);
+
+ return err;
+}
+
+
+
+/*
+ * Command line interface to update (used by checkout)
+ *
+ * repository = cvsroot->repository + update_dir. This is necessary for
+ * checkout so that start_recursion can determine our repository. In the
+ * update case, start_recursion can use the CVS/Root & CVS/Repository file
+ * to determine this value.
+ */
+int
+do_update (int argc, char **argv, char *xoptions, char *xtag, char *xdate,
+ int xforce, int local, int xbuild, int xaflag, int xprune,
+ int xpipeout, int which, char *xjoin_rev1, char *xjoin_date1,
+ char *xjoin_rev2, char *xjoin_date2,
+ char *preload_update_dir, int xdotemplate, char *repository)
+{
+ int err = 0;
+
+ TRACE (TRACE_FUNCTION,
+"do_update (%s, %s, %s, %d, %d, %d, %d, %d, %d, %d, %s, %s, %s, %s, %s, %d, %s)",
+ xoptions ? xoptions : "(null)", xtag ? xtag : "(null)",
+ xdate ? xdate : "(null)", xforce, local, xbuild, xaflag, xprune,
+ xpipeout, which, xjoin_rev1 ? xjoin_rev1 : "(null)",
+ xjoin_date1 ? xjoin_date1 : "(null)",
+ xjoin_rev2 ? xjoin_rev2 : "(null)",
+ xjoin_date2 ? xjoin_date2 : "(null)",
+ preload_update_dir ? preload_update_dir : "(null)", xdotemplate,
+ repository ? repository : "(null)");
+
+ /* fill in the statics */
+ options = xoptions;
+ tag = xtag;
+ date = xdate;
+ force_tag_match = xforce;
+ update_build_dirs = xbuild;
+ aflag = xaflag;
+ update_prune_dirs = xprune;
+ pipeout = xpipeout;
+ dotemplate = xdotemplate;
+
+ /* setup the join support */
+ join_rev1 = xjoin_rev1;
+ join_date1 = xjoin_date1;
+ join_rev2 = xjoin_rev2;
+ join_date2 = xjoin_date2;
+
+#ifdef PRESERVE_PERMISSIONS_SUPPORT
+ if (preserve_perms)
+ {
+ /* We need to do an extra recursion, bleah. It's to make sure
+ that we know as much as possible about file linkage. */
+ hardlist = getlist();
+ working_dir = xgetcwd (); /* save top-level working dir */
+
+ /* FIXME-twp: the arguments to start_recursion make me dizzy. This
+ function call was copied from the update_fileproc call that
+ follows it; someone should make sure that I did it right. */
+ err = start_recursion
+ (get_linkinfo_proc, NULL, NULL, NULL, NULL,
+ argc, argv, local, which, aflag, CVS_LOCK_READ,
+ preload_update_dir, 1, NULL);
+ if (err)
+ return err;
+
+ /* FIXME-twp: at this point we should walk the hardlist
+ and update the `links' field of each hardlink_info struct
+ to list the files that are linked on dist. That would make
+ it easier & more efficient to compare the disk linkage with
+ the repository linkage (a simple strcmp). */
+ }
+#endif
+
+ /* call the recursion processor */
+ err = start_recursion (update_fileproc, update_filesdone_proc,
+ update_dirent_proc, update_dirleave_proc, NULL,
+ argc, argv, local, which, aflag, CVS_LOCK_READ,
+ preload_update_dir, 1, repository);
+
+ /* see if we need to sleep before returning to avoid time-stamp races */
+ if (!server_active && last_register_time)
+ {
+ sleep_past (last_register_time);
+ }
+
+ return err;
+}
+
+
+
+#ifdef PRESERVE_PERMISSIONS_SUPPORT
+/*
+ * The get_linkinfo_proc callback adds each file to the hardlist
+ * (see hardlink.c).
+ */
+
+static int
+get_linkinfo_proc (void *callerdat, struct file_info *finfo)
+{
+ char *fullpath;
+ Node *linkp;
+ struct hardlink_info *hlinfo;
+
+ /* Get the full pathname of the current file. */
+ fullpath = Xasprintf ("%s/%s", working_dir, finfo->fullname);
+
+ /* To permit recursing into subdirectories, files
+ are keyed on the full pathname and not on the basename. */
+ linkp = lookup_file_by_inode (fullpath);
+ if (linkp == NULL)
+ {
+ /* The file isn't on disk; we are probably restoring
+ a file that was removed. */
+ return 0;
+ }
+
+ /* Create a new, empty hardlink_info node. */
+ hlinfo = xmalloc (sizeof (struct hardlink_info));
+
+ hlinfo->status = (Ctype) 0; /* is this dumb? */
+ hlinfo->checked_out = 0;
+
+ linkp->data = hlinfo;
+
+ return 0;
+}
+#endif
+
+
+
+/*
+ * This is the callback proc for update. It is called for each file in each
+ * directory by the recursion code. The current directory is the local
+ * instantiation. file is the file name we are to operate on. update_dir is
+ * set to the path relative to where we started (for pretty printing).
+ * repository is the repository. entries and srcfiles are the pre-parsed
+ * entries and source control files.
+ *
+ * This routine decides what needs to be done for each file and does the
+ * appropriate magic for checkout
+ */
+static int
+update_fileproc (void *callerdat, struct file_info *finfo)
+{
+ int retval, nb;
+ Ctype status;
+ Vers_TS *vers;
+
+ status = Classify_File (finfo, tag, date, options, force_tag_match,
+ aflag, &vers, pipeout);
+
+ /* Keep track of whether TAG is a branch tag.
+ Note that if it is a branch tag in some files and a nonbranch tag
+ in others, treat it as a nonbranch tag. */
+ if (rewrite_tag
+ && tag != NULL
+ && finfo->rcs != NULL)
+ {
+ char *rev = RCS_getversion (finfo->rcs, tag, NULL, 1, NULL);
+ if (rev != NULL
+ && nonbranch != (nb = !RCS_nodeisbranch (finfo->rcs, tag)))
+ {
+ if (nonbranch >= 0 && !warned && !quiet)
+ {
+ error (0, 0,
+"warning: %s is a branch tag in some files and a revision tag in others.",
+ tag);
+ warned = 1;
+ }
+ if (nonbranch < nb) nonbranch = nb;
+ }
+ if (rev != NULL)
+ free (rev);
+ }
+
+ if (pipeout)
+ {
+ /*
+ * We just return success without doing anything if any of the really
+ * funky cases occur
+ *
+ * If there is still a valid RCS file, do a regular checkout type
+ * operation
+ */
+ switch (status)
+ {
+ case T_UNKNOWN: /* unknown file was explicitly asked
+ * about */
+ case T_REMOVE_ENTRY: /* needs to be un-registered */
+ case T_ADDED: /* added but not committed */
+ retval = 0;
+ break;
+ case T_CONFLICT: /* old punt-type errors */
+ retval = 1;
+ break;
+ case T_UPTODATE: /* file was already up-to-date */
+ case T_NEEDS_MERGE: /* needs merging */
+ case T_MODIFIED: /* locally modified */
+ case T_REMOVED: /* removed but not committed */
+ case T_CHECKOUT: /* needs checkout */
+ case T_PATCH: /* needs patch */
+ retval = checkout_file (finfo, vers, 0, 0, 0);
+ break;
+
+ default: /* can't ever happen :-) */
+ error (0, 0,
+ "unknown file status %d for file %s", status, finfo->file);
+ retval = 0;
+ break;
+ }
+ }
+ else
+ {
+ switch (status)
+ {
+ case T_UNKNOWN: /* unknown file was explicitly asked
+ * about */
+ case T_UPTODATE: /* file was already up-to-date */
+ retval = 0;
+ break;
+ case T_CONFLICT: /* old punt-type errors */
+ retval = 1;
+ write_letter (finfo, 'C');
+ break;
+ case T_NEEDS_MERGE: /* needs merging */
+ if (! toss_local_changes)
+ {
+ retval = merge_file (finfo, vers);
+ break;
+ }
+ /* else FALL THROUGH */
+ case T_MODIFIED: /* locally modified */
+ retval = 0;
+ if (toss_local_changes)
+ {
+ char *bakname;
+ bakname = backup_file (finfo->file, vers->vn_user);
+ /* This behavior is sufficiently unexpected to
+ justify overinformativeness, I think. */
+ if (!really_quiet && !server_active)
+ (void) printf ("(Locally modified %s moved to %s)\n",
+ finfo->file, bakname);
+ free (bakname);
+
+ /* The locally modified file is still present, but
+ it will be overwritten by the repository copy
+ after this. */
+ status = T_CHECKOUT;
+ retval = checkout_file (finfo, vers, 0, 0, 1);
+ }
+ else
+ {
+ if (vers->ts_conflict)
+ {
+ if (file_has_markers (finfo))
+ {
+ write_letter (finfo, 'C');
+ retval = 1;
+ }
+ else
+ {
+ /* Reregister to clear conflict flag. */
+ Register (finfo->entries, finfo->file,
+ vers->vn_rcs, vers->ts_rcs,
+ vers->options, vers->tag,
+ vers->date, NULL);
+ }
+ }
+ if (!retval)
+ write_letter (finfo, 'M');
+ }
+ break;
+ case T_PATCH: /* needs patch */
+#ifdef SERVER_SUPPORT
+ if (patches)
+ {
+ int docheckout;
+ struct stat file_info;
+ unsigned char checksum[16];
+
+ retval = patch_file (finfo,
+ vers, &docheckout,
+ &file_info, checksum);
+ if (! docheckout)
+ {
+ if (server_active && retval == 0)
+ server_updated (finfo, vers,
+ (rcs_diff_patches
+ ? SERVER_RCS_DIFF
+ : SERVER_PATCHED),
+ file_info.st_mode, checksum,
+ NULL);
+ break;
+ }
+ }
+#endif
+ /* If we're not running as a server, just check the
+ file out. It's simpler and faster than producing
+ and applying patches. */
+ /* Fall through. */
+ case T_CHECKOUT: /* needs checkout */
+ retval = checkout_file (finfo, vers, 0, 0, 1);
+ break;
+ case T_ADDED: /* added but not committed */
+ write_letter (finfo, 'A');
+ retval = 0;
+ break;
+ case T_REMOVED: /* removed but not committed */
+ write_letter (finfo, 'R');
+ retval = 0;
+ break;
+ case T_REMOVE_ENTRY: /* needs to be un-registered */
+ retval = scratch_file (finfo, vers);
+ break;
+ default: /* can't ever happen :-) */
+ error (0, 0,
+ "unknown file status %d for file %s", status, finfo->file);
+ retval = 0;
+ break;
+ }
+ }
+
+ /* only try to join if things have gone well thus far */
+ if (retval == 0 && join_rev1)
+ join_file (finfo, vers);
+
+ /* if this directory has an ignore list, add this file to it */
+ if (ignlist && (status != T_UNKNOWN || vers->ts_user == NULL))
+ {
+ Node *p;
+
+ p = getnode ();
+ p->type = FILES;
+ p->key = xstrdup (finfo->file);
+ if (addnode (ignlist, p) != 0)
+ freenode (p);
+ }
+
+ freevers_ts (&vers);
+ return retval;
+}
+
+
+
+static void
+update_ignproc (const char *file, const char *dir)
+{
+ struct file_info finfo;
+ char *tmp;
+
+ memset (&finfo, 0, sizeof (finfo));
+ finfo.file = file;
+ finfo.update_dir = dir;
+
+ finfo.fullname = tmp = Xasprintf ("%s%s%s",
+ dir[0] == '\0' ? "" : dir,
+ dir[0] == '\0' ? "" : "/",
+ file);
+ write_letter (&finfo, '?');
+ free (tmp);
+}
+
+
+
+/* ARGSUSED */
+static int
+update_filesdone_proc (void *callerdat, int err, const char *repository,
+ const char *update_dir, List *entries)
+{
+ if (nonbranch < 0) nonbranch = 0;
+ if (rewrite_tag)
+ {
+ WriteTag (NULL, tag, date, nonbranch, update_dir, repository);
+ rewrite_tag = 0;
+ }
+
+ /* if this directory has an ignore list, process it then free it */
+ if (ignlist)
+ {
+ ignore_files (ignlist, entries, update_dir, update_ignproc);
+ dellist (&ignlist);
+ }
+
+ /* Clean up CVS admin dirs if we are export */
+ if (strcmp (cvs_cmd_name, "export") == 0)
+ {
+ /* I'm not sure the existence_error is actually possible (except
+ in cases where we really should print a message), but since
+ this code used to ignore all errors, I'll play it safe. */
+ if (unlink_file_dir (CVSADM) < 0 && !existence_error (errno))
+ error (0, errno, "cannot remove %s directory", CVSADM);
+ }
+ else if (!server_active && !pipeout)
+ {
+ /* If there is no CVS/Root file, add one */
+ if (!isfile (CVSADM_ROOT))
+ Create_Root (NULL, original_parsed_root->original);
+ }
+
+ return err;
+}
+
+
+
+/*
+ * update_dirent_proc () is called back by the recursion processor before a
+ * sub-directory is processed for update. In this case, update_dirent proc
+ * will probably create the directory unless -d isn't specified and this is a
+ * new directory. 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
+update_dirent_proc (void *callerdat, const char *dir, const char *repository,
+ const char *update_dir, List *entries)
+{
+ if (ignore_directory (update_dir))
+ {
+ /* print the warm fuzzy message */
+ if (!quiet)
+ error (0, 0, "Ignoring %s", update_dir);
+ return R_SKIP_ALL;
+ }
+
+ if (!isdir (dir))
+ {
+ /* if we aren't building dirs, blow it off */
+ if (!update_build_dirs)
+ return R_SKIP_ALL;
+
+ /* Various CVS administrators are in the habit of removing
+ the repository directory for things they don't want any
+ more. I've even been known to do it myself (on rare
+ occasions). Not the usual recommended practice, but we
+ want to try to come up with some kind of
+ reasonable/documented/sensible behavior. Generally
+ the behavior is to just skip over that directory (see
+ dirs test in sanity.sh; the case which reaches here
+ is when update -d is specified, and the working directory
+ is gone but the subdirectory is still mentioned in
+ CVS/Entries). */
+ /* In the remote case, the client should refrain from
+ sending us the directory in the first place. So we
+ want to continue to give an error, so clients make
+ sure to do this. */
+ if (!server_active && !isdir (repository))
+ return R_SKIP_ALL;
+
+ if (noexec)
+ {
+ /* ignore the missing dir if -n is specified */
+ error (0, 0, "New directory `%s' -- ignored", update_dir);
+ return R_SKIP_ALL;
+ }
+ else
+ {
+ /* otherwise, create the dir and appropriate adm files */
+
+ /* If no tag or date were specified on the command line,
+ and we're not using -A, we want the subdirectory to use
+ the tag and date, if any, of the current directory.
+ That way, update -d will work correctly when working on
+ a branch.
+
+ We use TAG_UPDATE_DIR to undo the tag setting in
+ update_dirleave_proc. If we did not do this, we would
+ not correctly handle a working directory with multiple
+ tags (and maybe we should prohibit such working
+ directories, but they work now and we shouldn't make
+ them stop working without more thought). */
+ if ((tag == NULL && date == NULL) && ! aflag)
+ {
+ ParseTag (&tag, &date, &nonbranch);
+ if (tag != NULL || date != NULL)
+ tag_update_dir = xstrdup (update_dir);
+ }
+
+ make_directory (dir);
+ Create_Admin (dir, update_dir, repository, tag, date,
+ /* This is a guess. We will rewrite it later
+ via WriteTag. */
+ 0,
+ 0,
+ dotemplate);
+ rewrite_tag = 1;
+ nonbranch = -1;
+ warned = 0;
+ Subdir_Register (entries, NULL, dir);
+ }
+ }
+ /* Do we need to check noexec here? */
+ else if (!pipeout)
+ {
+ char *cvsadmdir;
+
+ /* The directory exists. Check to see if it has a CVS
+ subdirectory. */
+
+ cvsadmdir = Xasprintf ("%s/%s", dir, CVSADM);
+
+ if (!isdir (cvsadmdir))
+ {
+ /* We cannot successfully recurse into a directory without a CVS
+ subdirectory. Generally we will have already printed
+ "? foo". */
+ free (cvsadmdir);
+ return R_SKIP_ALL;
+ }
+ free (cvsadmdir);
+ }
+
+ /*
+ * If we are building dirs and not going to stdout, we make sure there is
+ * no static entries file and write the tag file as appropriate
+ */
+ if (!pipeout)
+ {
+ if (update_build_dirs)
+ {
+ char *tmp = Xasprintf ("%s/%s", dir, CVSADM_ENTSTAT);
+
+ if (unlink_file (tmp) < 0 && ! existence_error (errno))
+ error (1, errno, "cannot remove file %s", tmp);
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ server_clear_entstat (update_dir, repository);
+#endif
+ free (tmp);
+ }
+
+ /* keep the CVS/Tag file current with the specified arguments */
+ if (aflag || tag || date)
+ {
+ WriteTag (dir, tag, date, 0, update_dir, repository);
+ rewrite_tag = 1;
+ nonbranch = -1;
+ warned = 0;
+ }
+
+ WriteTemplate (update_dir, dotemplate, repository);
+
+ /* initialize the ignore list for this directory */
+ ignlist = getlist ();
+ }
+
+ /* print the warm fuzzy message */
+ if (!quiet)
+ error (0, 0, "Updating %s", update_dir);
+
+ return R_PROCESS;
+}
+
+
+
+/*
+ * update_dirleave_proc () is called back by the recursion code upon leaving
+ * a directory. It will prune empty directories if needed and will execute
+ * any appropriate update programs.
+ */
+/* ARGSUSED */
+static int
+update_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);
+
+ /* If we set the tag or date for a new subdirectory in
+ update_dirent_proc, and we're now done with that subdirectory,
+ undo the tag/date setting. Note that we know that the tag and
+ date were both originally NULL in this case. */
+ if (tag_update_dir != NULL && strcmp (update_dir, tag_update_dir) == 0)
+ {
+ if (tag != NULL)
+ {
+ free (tag);
+ tag = NULL;
+ }
+ if (date != NULL)
+ {
+ free (date);
+ date = NULL;
+ }
+ nonbranch = -1;
+ warned = 0;
+ free (tag_update_dir);
+ tag_update_dir = NULL;
+ }
+
+ if (strchr (dir, '/') == NULL)
+ {
+ /* FIXME: chdir ("..") loses with symlinks. */
+ /* Prune empty dirs on the way out - if necessary */
+ (void) CVS_CHDIR ("..");
+ if (update_prune_dirs && isemptydir (dir, 0))
+ {
+ /* I'm not sure the existence_error is actually possible (except
+ in cases where we really should print a message), but since
+ this code used to ignore all errors, I'll play it safe. */
+ if (unlink_file_dir (dir) < 0 && !existence_error (errno))
+ error (0, errno, "cannot remove %s directory", dir);
+ Subdir_Deregister (entries, NULL, dir);
+ }
+ }
+
+ return err;
+}
+
+
+
+/* Returns 1 if the file indicated by node has been removed. */
+static int
+isremoved (Node *node, void *closure)
+{
+ Entnode *entdata = node->data;
+
+ /* If the first character of the version is a '-', the file has been
+ removed. */
+ return (entdata->version && entdata->version[0] == '-') ? 1 : 0;
+}
+
+
+
+/* Returns 1 if the argument directory is completely empty, other than the
+ existence of the CVS directory entry. Zero otherwise. If MIGHT_NOT_EXIST
+ and the directory doesn't exist, then just return 0. */
+int
+isemptydir (const char *dir, int might_not_exist)
+{
+ DIR *dirp;
+ struct dirent *dp;
+
+ if ((dirp = CVS_OPENDIR (dir)) == NULL)
+ {
+ if (might_not_exist && existence_error (errno))
+ return 0;
+ error (0, errno, "cannot open directory %s for empty check", dir);
+ return 0;
+ }
+ errno = 0;
+ while ((dp = CVS_READDIR (dirp)) != NULL)
+ {
+ if (strcmp (dp->d_name, ".") != 0
+ && strcmp (dp->d_name, "..") != 0)
+ {
+ if (strcmp (dp->d_name, CVSADM) != 0)
+ {
+ /* An entry other than the CVS directory. The directory
+ is certainly not empty. */
+ (void) CVS_CLOSEDIR (dirp);
+ return 0;
+ }
+ else
+ {
+ /* The CVS directory entry. We don't have to worry about
+ this unless the Entries file indicates that files have
+ been removed, but not committed, in this directory.
+ (Removing the directory would prevent people from
+ comitting the fact that they removed the files!) */
+ List *l;
+ int files_removed;
+ struct saved_cwd cwd;
+
+ if (save_cwd (&cwd))
+ error (1, errno, "Failed to save current directory.");
+
+ if (CVS_CHDIR (dir) < 0)
+ error (1, errno, "cannot change directory to %s", dir);
+ l = Entries_Open (0, NULL);
+ files_removed = walklist (l, isremoved, 0);
+ Entries_Close (l);
+
+ if (restore_cwd (&cwd))
+ error (1, errno,
+ "Failed to restore current directory, `%s'.",
+ cwd.name);
+ free_cwd (&cwd);
+
+ if (files_removed != 0)
+ {
+ /* There are files that have been removed, but not
+ committed! Do not consider the directory empty. */
+ (void) CVS_CLOSEDIR (dirp);
+ return 0;
+ }
+ }
+ }
+ errno = 0;
+ }
+ if (errno != 0)
+ {
+ error (0, errno, "cannot read directory %s", dir);
+ (void) CVS_CLOSEDIR (dirp);
+ return 0;
+ }
+ (void) CVS_CLOSEDIR (dirp);
+ return 1;
+}
+
+
+
+/*
+ * scratch the Entries file entry associated with a file
+ */
+static int
+scratch_file (struct file_info *finfo, Vers_TS *vers)
+{
+ history_write ('W', finfo->update_dir, "", finfo->file, finfo->repository);
+ Scratch_Entry (finfo->entries, finfo->file);
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ {
+ if (vers->ts_user == NULL)
+ server_scratch_entry_only ();
+ server_updated (finfo, vers, SERVER_UPDATED, (mode_t) -1, NULL, NULL);
+ }
+#endif
+ if (unlink_file (finfo->file) < 0 && ! existence_error (errno))
+ error (0, errno, "unable to remove %s", finfo->fullname);
+ else if (!server_active)
+ {
+ /* skip this step when the server is running since
+ * server_updated should have handled it */
+ /* keep the vers structure up to date in case we do a join
+ * - if there isn't a file, it can't very well have a version number, can it?
+ */
+ if (vers->vn_user != NULL)
+ {
+ free (vers->vn_user);
+ vers->vn_user = NULL;
+ }
+ if (vers->ts_user != NULL)
+ {
+ free (vers->ts_user);
+ vers->ts_user = NULL;
+ }
+ }
+ return 0;
+}
+
+
+
+/*
+ * Check out a file.
+ */
+static int
+checkout_file (struct file_info *finfo, Vers_TS *vers_ts, int adding,
+ int merging, int update_server)
+{
+ char *backup;
+ int set_time, retval = 0;
+ int status;
+ int file_is_dead;
+ struct buffer *revbuf;
+
+ backup = NULL;
+ revbuf = NULL;
+
+ /* Don't screw with backup files if we're going to stdout, or if
+ we are the server. */
+ if (!pipeout && !server_active)
+ {
+ backup = Xasprintf ("%s/%s%s", CVSADM, CVSPREFIX, finfo->file);
+ if (isfile (finfo->file))
+ rename_file (finfo->file, backup);
+ else
+ {
+ /* If -f/-t wrappers are being used to wrap up a directory,
+ then backup might be a directory instead of just a file. */
+ if (unlink_file_dir (backup) < 0)
+ {
+ /* Not sure if the existence_error check is needed here. */
+ if (!existence_error (errno))
+ /* FIXME: should include update_dir in message. */
+ error (0, errno, "error removing %s", backup);
+ }
+ free (backup);
+ backup = NULL;
+ }
+ }
+
+ file_is_dead = RCS_isdead (vers_ts->srcfile, vers_ts->vn_rcs);
+
+ if (!file_is_dead)
+ {
+ /*
+ * if we are checking out to stdout, print a nice message to
+ * stderr, and add the -p flag to the command */
+ if (pipeout)
+ {
+ if (!quiet)
+ {
+ cvs_outerr ("\
+===================================================================\n\
+Checking out ", 0);
+ cvs_outerr (finfo->fullname, 0);
+ cvs_outerr ("\n\
+RCS: ", 0);
+ cvs_outerr (vers_ts->srcfile->print_path, 0);
+ cvs_outerr ("\n\
+VERS: ", 0);
+ cvs_outerr (vers_ts->vn_rcs, 0);
+ cvs_outerr ("\n***************\n", 0);
+ }
+ }
+
+#ifdef SERVER_SUPPORT
+ if (update_server
+ && server_active
+ && ! pipeout
+ && ! file_gzip_level
+ && ! joining ()
+ && ! wrap_name_has (finfo->file, WRAP_FROMCVS))
+ {
+ revbuf = buf_nonio_initialize (NULL);
+ status = RCS_checkout (vers_ts->srcfile, NULL,
+ vers_ts->vn_rcs, vers_ts->tag,
+ vers_ts->options, RUN_TTY,
+ checkout_to_buffer, revbuf);
+ }
+ else
+#endif
+ status = RCS_checkout (vers_ts->srcfile,
+ pipeout ? NULL : finfo->file,
+ vers_ts->vn_rcs, vers_ts->tag,
+ vers_ts->options, RUN_TTY, NULL, NULL);
+ }
+ if (file_is_dead || status == 0)
+ {
+ mode_t mode;
+
+ mode = (mode_t) -1;
+
+ if (!pipeout)
+ {
+ Vers_TS *xvers_ts;
+
+ if (revbuf != NULL && !noexec)
+ {
+ struct stat sb;
+
+ /* FIXME: We should have RCS_checkout return the mode.
+ That would also fix the kludge with noexec, above, which
+ is here only because noexec doesn't write srcfile->path
+ for us to stat. */
+ if (stat (vers_ts->srcfile->path, &sb) < 0)
+ {
+#if defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT)
+ buf_free (revbuf);
+#endif /* defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT) */
+ error (1, errno, "cannot stat %s",
+ vers_ts->srcfile->path);
+ }
+ mode = sb.st_mode &~ (S_IWRITE | S_IWGRP | S_IWOTH);
+ }
+
+ if (cvswrite
+ && !file_is_dead
+ && !fileattr_get (finfo->file, "_watched"))
+ {
+ if (revbuf == NULL)
+ xchmod (finfo->file, 1);
+ else
+ {
+ /* We know that we are the server here, so
+ although xchmod checks umask, we don't bother. */
+ mode |= (((mode & S_IRUSR) ? S_IWUSR : 0)
+ | ((mode & S_IRGRP) ? S_IWGRP : 0)
+ | ((mode & S_IROTH) ? S_IWOTH : 0));
+ }
+ }
+
+ {
+ /* A newly checked out file is never under the spell
+ of "cvs edit". If we think we were editing it
+ from a previous life, clean up. Would be better to
+ check for same the working directory instead of
+ same user, but that is hairy. */
+
+ struct addremove_args args;
+
+ editor_set (finfo->file, getcaller (), NULL);
+
+ memset (&args, 0, sizeof args);
+ args.remove_temp = 1;
+ watch_modify_watchers (finfo->file, &args);
+ }
+
+ /* set the time from the RCS file iff it was unknown before */
+ set_time =
+ (!noexec
+ && (vers_ts->vn_user == NULL ||
+ strncmp (vers_ts->ts_rcs, "Initial", 7) == 0)
+ && !file_is_dead);
+
+ wrap_fromcvs_process_file (finfo->file);
+
+ xvers_ts = Version_TS (finfo, options, tag, date,
+ force_tag_match, set_time);
+ if (strcmp (xvers_ts->options, "-V4") == 0)
+ xvers_ts->options[0] = '\0';
+
+ if (revbuf != NULL)
+ {
+ /* If we stored the file data into a buffer, then we
+ didn't create a file at all, so xvers_ts->ts_user
+ is wrong. The correct value is to have it be the
+ same as xvers_ts->ts_rcs, meaning that the working
+ file is unchanged from the RCS file.
+
+ FIXME: We should tell Version_TS not to waste time
+ statting the nonexistent file.
+
+ FIXME: Actually, I don't think the ts_user value
+ matters at all here. The only use I know of is
+ that it is printed in a trace message by
+ Server_Register. */
+
+ if (xvers_ts->ts_user != NULL)
+ free (xvers_ts->ts_user);
+ xvers_ts->ts_user = xstrdup (xvers_ts->ts_rcs);
+ }
+
+ (void) time (&last_register_time);
+
+ if (file_is_dead)
+ {
+ if (xvers_ts->vn_user != NULL)
+ {
+ error (0, 0,
+ "warning: %s is not (any longer) pertinent",
+ finfo->fullname);
+ }
+ Scratch_Entry (finfo->entries, finfo->file);
+#ifdef SERVER_SUPPORT
+ if (server_active && xvers_ts->ts_user == NULL)
+ server_scratch_entry_only ();
+#endif
+ /* FIXME: Rather than always unlink'ing, and ignoring the
+ existence_error, we should do the unlink only if
+ vers_ts->ts_user is non-NULL. Then there would be no
+ need to ignore an existence_error (for example, if the
+ user removes the file while we are running). */
+ if (unlink_file (finfo->file) < 0 && ! existence_error (errno))
+ {
+ error (0, errno, "cannot remove %s", finfo->fullname);
+ }
+ }
+ else
+ Register (finfo->entries, finfo->file,
+ adding ? "0" : xvers_ts->vn_rcs,
+ xvers_ts->ts_user, xvers_ts->options,
+ xvers_ts->tag, xvers_ts->date,
+ NULL); /* Clear conflict flag on fresh checkout */
+
+ /* fix up the vers structure, in case it is used by join */
+ if (join_rev1)
+ {
+ /* FIXME: Throwing away the original revision info is almost
+ certainly wrong -- what if join_rev1 is "BASE"? */
+ if (vers_ts->vn_user != NULL)
+ free (vers_ts->vn_user);
+ if (vers_ts->vn_rcs != NULL)
+ free (vers_ts->vn_rcs);
+ vers_ts->vn_user = xstrdup (xvers_ts->vn_rcs);
+ vers_ts->vn_rcs = xstrdup (xvers_ts->vn_rcs);
+ }
+
+ /* If this is really Update and not Checkout, recode history */
+ if (strcmp (cvs_cmd_name, "update") == 0)
+ history_write ('U', finfo->update_dir, xvers_ts->vn_rcs, finfo->file,
+ finfo->repository);
+
+ freevers_ts (&xvers_ts);
+
+ if (!really_quiet && !file_is_dead)
+ {
+ write_letter (finfo, 'U');
+ }
+ }
+
+#ifdef SERVER_SUPPORT
+ if (update_server && server_active)
+ server_updated (finfo, vers_ts,
+ merging ? SERVER_MERGED : SERVER_UPDATED,
+ mode, NULL, revbuf);
+#endif
+ }
+ else
+ {
+ if (backup != NULL)
+ {
+ rename_file (backup, finfo->file);
+ free (backup);
+ backup = NULL;
+ }
+
+ error (0, 0, "could not check out %s", finfo->fullname);
+
+ retval = status;
+ }
+
+ if (backup != NULL)
+ {
+ /* If -f/-t wrappers are being used to wrap up a directory,
+ then backup might be a directory instead of just a file. */
+ if (unlink_file_dir (backup) < 0)
+ {
+ /* Not sure if the existence_error check is needed here. */
+ if (!existence_error (errno))
+ /* FIXME: should include update_dir in message. */
+ error (0, errno, "error removing %s", backup);
+ }
+ free (backup);
+ }
+
+#if defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT)
+ if (revbuf != NULL)
+ buf_free (revbuf);
+#endif /* defined (SERVER_SUPPORT) || defined (CLIENT_SUPPORT) */
+ return retval;
+}
+
+
+
+#ifdef SERVER_SUPPORT
+
+/* This function is used to write data from a file being checked out
+ into a buffer. */
+
+static void
+checkout_to_buffer (void *callerdat, const char *data, size_t len)
+{
+ struct buffer *buf = (struct buffer *) callerdat;
+
+ buf_output (buf, data, len);
+}
+
+#endif /* SERVER_SUPPORT */
+
+#ifdef SERVER_SUPPORT
+
+/* This structure is used to pass information between patch_file and
+ patch_file_write. */
+
+struct patch_file_data
+{
+ /* File name, for error messages. */
+ const char *filename;
+ /* File to which to write. */
+ FILE *fp;
+ /* Whether to compute the MD5 checksum. */
+ int compute_checksum;
+ /* Data structure for computing the MD5 checksum. */
+ struct md5_ctx context;
+ /* Set if the file has a final newline. */
+ int final_nl;
+};
+
+/* Patch a file. Runs diff. This is only done when running as the
+ * server. The hope is that the diff will be smaller than the file
+ * itself.
+ */
+static int
+patch_file (struct file_info *finfo, Vers_TS *vers_ts, int *docheckout,
+ struct stat *file_info, unsigned char *checksum)
+{
+ char *backup;
+ char *file1;
+ char *file2;
+ int retval = 0;
+ int retcode = 0;
+ int fail;
+ FILE *e;
+ struct patch_file_data data;
+
+ *docheckout = 0;
+
+ if (noexec || pipeout || joining ())
+ {
+ *docheckout = 1;
+ return 0;
+ }
+
+ /* If this file has been marked as being binary, then never send a
+ patch. */
+ if (strcmp (vers_ts->options, "-kb") == 0)
+ {
+ *docheckout = 1;
+ return 0;
+ }
+
+ /* First check that the first revision exists. If it has been nuked
+ by cvs admin -o, then just fall back to checking out entire
+ revisions. In some sense maybe we don't have to do this; after
+ all cvs.texinfo says "Make sure that no-one has checked out a
+ copy of the revision you outdate" but then again, that advice
+ doesn't really make complete sense, because "cvs admin" operates
+ on a working directory and so _someone_ will almost always have
+ _some_ revision checked out. */
+ {
+ char *rev;
+
+ rev = RCS_gettag (finfo->rcs, vers_ts->vn_user, 1, NULL);
+ if (rev == NULL)
+ {
+ *docheckout = 1;
+ return 0;
+ }
+ else
+ free (rev);
+ }
+
+ /* If the revision is dead, let checkout_file handle it rather
+ than duplicating the processing here. */
+ if (RCS_isdead (vers_ts->srcfile, vers_ts->vn_rcs))
+ {
+ *docheckout = 1;
+ return 0;
+ }
+
+ backup = Xasprintf ("%s/%s%s", CVSADM, CVSPREFIX, finfo->file);
+ if (isfile (finfo->file))
+ rename_file (finfo->file, backup);
+ else
+ {
+ if (unlink_file (backup) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s", backup);
+ }
+
+ file1 = Xasprintf ("%s/%s%s-1", CVSADM, CVSPREFIX, finfo->file);
+ file2 = Xasprintf ("%s/%s%s-2", CVSADM, CVSPREFIX, finfo->file);
+
+ fail = 0;
+
+ /* We need to check out both revisions first, to see if either one
+ has a trailing newline. Because of this, we don't use rcsdiff,
+ but just use diff. */
+
+ e = CVS_FOPEN (file1, "w");
+ if (e == NULL)
+ error (1, errno, "cannot open %s", file1);
+
+ data.filename = file1;
+ data.fp = e;
+ data.final_nl = 0;
+ data.compute_checksum = 0;
+
+ /* FIXME - Passing vers_ts->tag here is wrong in the least number
+ * of cases. Since we don't know whether vn_user was checked out
+ * using a tag, we pass vers_ts->tag, which, assuming the user did
+ * not specify a new TAG to -r, will be the branch we are on.
+ *
+ * The only thing it is used for is to substitute in for the Name
+ * RCS keyword, so in the error case, the patch fails to apply on
+ * the client end and we end up resending the whole file.
+ *
+ * At least, if we are keeping track of the tag vn_user came from,
+ * I don't know where yet. -DRP
+ */
+ retcode = RCS_checkout (vers_ts->srcfile, NULL,
+ vers_ts->vn_user, vers_ts->tag,
+ vers_ts->options, RUN_TTY,
+ patch_file_write, (void *) &data);
+
+ if (fclose (e) < 0)
+ error (1, errno, "cannot close %s", file1);
+
+ if (retcode != 0 || ! data.final_nl)
+ fail = 1;
+
+ if (! fail)
+ {
+ e = CVS_FOPEN (file2, "w");
+ if (e == NULL)
+ error (1, errno, "cannot open %s", file2);
+
+ data.filename = file2;
+ data.fp = e;
+ data.final_nl = 0;
+ data.compute_checksum = 1;
+ md5_init_ctx (&data.context);
+
+ retcode = RCS_checkout (vers_ts->srcfile, NULL,
+ vers_ts->vn_rcs, vers_ts->tag,
+ vers_ts->options, RUN_TTY,
+ patch_file_write, (void *) &data);
+
+ if (fclose (e) < 0)
+ error (1, errno, "cannot close %s", file2);
+
+ if (retcode != 0 || ! data.final_nl)
+ fail = 1;
+ else
+ md5_finish_ctx (&data.context, checksum);
+ }
+
+ retcode = 0;
+ if (! fail)
+ {
+ int dargc = 0;
+ size_t darg_allocated = 0;
+ char **dargv = NULL;
+
+ /* If the client does not support the Rcs-diff command, we
+ send a context diff, and the client must invoke patch.
+ That approach was problematical for various reasons. The
+ new approach only requires running diff in the server; the
+ client can handle everything without invoking an external
+ program. */
+ if (!rcs_diff_patches)
+ /* We use -c, not -u, because that is what CVS has
+ traditionally used. Kind of a moot point, now that
+ Rcs-diff is preferred, so there is no point in making
+ the compatibility issues worse. */
+ run_add_arg_p (&dargc, &darg_allocated, &dargv, "-c");
+ else
+ /* Now that diff is librarified, we could be passing -a if
+ we wanted to. However, it is unclear to me whether we
+ would want to. Does diff -a, in any significant
+ percentage of cases, produce patches which are smaller
+ than the files it is patching? I guess maybe text
+ files with character sets which diff regards as
+ 'binary'. Conversely, do they tend to be much larger
+ in the bad cases? This needs some more
+ thought/investigation, I suspect. */
+ run_add_arg_p (&dargc, &darg_allocated, &dargv, "-n");
+ retcode = diff_exec (file1, file2, NULL, NULL, dargc, dargv,
+ finfo->file);
+ run_arg_free_p (dargc, dargv);
+ free (dargv);
+
+ /* A retcode of 0 means no differences. 1 means some differences. */
+ if (retcode != 0 && retcode != 1)
+ fail = 1;
+ }
+
+ if (!fail)
+ {
+ struct stat file2_info;
+
+ /* Check to make sure the patch is really shorter */
+ if (stat (file2, &file2_info) < 0)
+ error (1, errno, "could not stat %s", file2);
+ if (stat (finfo->file, file_info) < 0)
+ error (1, errno, "could not stat %s", finfo->file);
+ if (file2_info.st_size <= file_info->st_size)
+ fail = 1;
+ }
+
+ if (! fail)
+ {
+# define BINARY "Binary"
+ char buf[sizeof BINARY];
+ unsigned int c;
+
+ /* Check the diff output to make sure patch will be handle it. */
+ e = CVS_FOPEN (finfo->file, "r");
+ if (e == NULL)
+ error (1, errno, "could not open diff output file %s",
+ finfo->fullname);
+ c = fread (buf, 1, sizeof BINARY - 1, e);
+ buf[c] = '\0';
+ if (strcmp (buf, BINARY) == 0)
+ {
+ /* These are binary files. We could use diff -a, but
+ patch can't handle that. */
+ fail = 1;
+ }
+ fclose (e);
+ }
+
+ if (! fail)
+ {
+ Vers_TS *xvers_ts;
+
+ /* Stat the original RCS file, and then adjust it the way
+ that RCS_checkout would. FIXME: This is an abstraction
+ violation. */
+ if (stat (vers_ts->srcfile->path, file_info) < 0)
+ error (1, errno, "could not stat %s", vers_ts->srcfile->path);
+ if (chmod (finfo->file,
+ file_info->st_mode & ~(S_IWRITE | S_IWGRP | S_IWOTH))
+ < 0)
+ error (0, errno, "cannot change mode of file %s", finfo->file);
+ if (cvswrite
+ && !fileattr_get (finfo->file, "_watched"))
+ xchmod (finfo->file, 1);
+
+ /* This stuff is just copied blindly from checkout_file. I
+ don't really know what it does. */
+ xvers_ts = Version_TS (finfo, options, tag, date,
+ force_tag_match, 0);
+ if (strcmp (xvers_ts->options, "-V4") == 0)
+ xvers_ts->options[0] = '\0';
+
+ Register (finfo->entries, finfo->file, xvers_ts->vn_rcs,
+ xvers_ts->ts_user, xvers_ts->options,
+ xvers_ts->tag, xvers_ts->date, NULL);
+
+ if (stat (finfo->file, file_info) < 0)
+ error (1, errno, "could not stat %s", finfo->file);
+
+ /* If this is really Update and not Checkout, record history. */
+ if (strcmp (cvs_cmd_name, "update") == 0)
+ history_write ('P', finfo->update_dir, xvers_ts->vn_rcs,
+ finfo->file, finfo->repository);
+
+ freevers_ts (&xvers_ts);
+
+ if (!really_quiet)
+ {
+ write_letter (finfo, 'P');
+ }
+ }
+ else
+ {
+ int old_errno = errno; /* save errno value over the rename */
+
+ if (isfile (backup))
+ rename_file (backup, finfo->file);
+
+ if (retcode != 0 && retcode != 1)
+ error (retcode == -1 ? 1 : 0, retcode == -1 ? old_errno : 0,
+ "could not diff %s", finfo->fullname);
+
+ *docheckout = 1;
+ retval = retcode;
+ }
+
+ if (unlink_file (backup) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s", backup);
+ if (unlink_file (file1) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s", file1);
+ if (unlink_file (file2) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s", file2);
+
+ free (backup);
+ free (file1);
+ free (file2);
+ return retval;
+}
+
+
+
+/* Write data to a file. Record whether the last byte written was a
+ newline. Optionally compute a checksum. This is called by
+ patch_file via RCS_checkout. */
+
+static void
+patch_file_write (void *callerdat, const char *buffer, size_t len)
+{
+ struct patch_file_data *data = (struct patch_file_data *) callerdat;
+
+ if (fwrite (buffer, 1, len, data->fp) != len)
+ error (1, errno, "cannot write %s", data->filename);
+
+ data->final_nl = (buffer[len - 1] == '\n');
+
+ if (data->compute_checksum)
+ md5_process_bytes (buffer, len, &data->context);
+}
+
+#endif /* SERVER_SUPPORT */
+
+/*
+ * Several of the types we process only print a bit of information consisting
+ * of a single letter and the name.
+ */
+void
+write_letter (struct file_info *finfo, int letter)
+{
+ if (!really_quiet)
+ {
+ char *tag = NULL;
+ /* Big enough for "+updated" or any of its ilk. */
+ char buf[80];
+
+ switch (letter)
+ {
+ case 'U':
+ tag = "updated";
+ break;
+ default:
+ /* We don't yet support tagged output except for "U". */
+ break;
+ }
+
+ if (tag != NULL)
+ {
+ sprintf (buf, "+%s", tag);
+ cvs_output_tagged (buf, NULL);
+ }
+ buf[0] = letter;
+ buf[1] = ' ';
+ buf[2] = '\0';
+ cvs_output_tagged ("text", buf);
+ cvs_output_tagged ("fname", finfo->fullname);
+ cvs_output_tagged ("newline", NULL);
+ if (tag != NULL)
+ {
+ sprintf (buf, "-%s", tag);
+ cvs_output_tagged (buf, NULL);
+ }
+ }
+ return;
+}
+
+
+
+/* Reregister a file after a merge. */
+static void
+RegisterMerge (struct file_info *finfo, Vers_TS *vers,
+ const char *backup, int has_conflicts)
+{
+ /* This file is the result of a merge, which means that it has
+ been modified. We use a special timestamp string which will
+ not compare equal to any actual timestamp. */
+ char *cp = NULL;
+
+ if (has_conflicts)
+ {
+ time (&last_register_time);
+ cp = time_stamp (finfo->file);
+ }
+ Register (finfo->entries, finfo->file, vers->vn_rcs ? vers->vn_rcs : "0",
+ "Result of merge", vers->options, vers->tag, vers->date, cp);
+ if (cp)
+ free (cp);
+
+#ifdef SERVER_SUPPORT
+ /* Send the new contents of the file before the message. If we
+ wanted to be totally correct, we would have the client write
+ the message only after the file has safely been written. */
+ if (server_active)
+ {
+ server_copy_file (finfo->file, finfo->update_dir, finfo->repository,
+ backup);
+ server_updated (finfo, vers, SERVER_MERGED, (mode_t) -1, NULL, NULL);
+ }
+#endif
+}
+
+
+
+/*
+ * Do all the magic associated with a file which needs to be merged
+ */
+static int
+merge_file (struct file_info *finfo, Vers_TS *vers)
+{
+ char *backup;
+ int status;
+ int retval;
+
+ assert (vers->vn_user);
+
+ /*
+ * The users currently modified file is moved to a backup file name
+ * ".#filename.version", so that it will stay around for a few days
+ * before being automatically removed by some cron daemon. The "version"
+ * is the version of the file that the user was most up-to-date with
+ * before the merge.
+ */
+ backup = Xasprintf ("%s%s.%s", BAKPREFIX, finfo->file, vers->vn_user);
+
+ if (unlink_file (backup) && !existence_error (errno))
+ error (0, errno, "unable to remove %s", backup);
+ copy_file (finfo->file, backup);
+ xchmod (finfo->file, 1);
+
+ if (strcmp (vers->options, "-kb") == 0
+ || wrap_merge_is_copy (finfo->file)
+ || special_file_mismatch (finfo, NULL, vers->vn_rcs))
+ {
+ /* For binary files, a merge is always a conflict. Same for
+ files whose permissions or linkage do not match. We give the
+ user the two files, and let them resolve it. It is possible
+ that we should require a "touch foo" or similar step before
+ we allow a checkin. */
+
+ /* TODO: it may not always be necessary to regard a permission
+ mismatch as a conflict. The working file and the RCS file
+ have a common ancestor `A'; if the working file's permissions
+ match A's, then it's probably safe to overwrite them with the
+ RCS permissions. Only if the working file, the RCS file, and
+ A all disagree should this be considered a conflict. But more
+ thought needs to go into this, and in the meantime it is safe
+ to treat any such mismatch as an automatic conflict. -twp */
+
+ status = RCS_checkout (finfo->rcs, finfo->file, vers->vn_rcs,
+ vers->tag, vers->options, NULL, NULL, NULL);
+ if (status)
+ {
+ error (0, 0, "failed to check out `%s' file", finfo->fullname);
+ error (0, 0, "restoring `%s' from backup file `%s'",
+ finfo->fullname, backup);
+ rename_file (backup, finfo->file);
+ retval = 1;
+ goto out;
+ }
+
+ xchmod (finfo->file, 1);
+
+ RegisterMerge (finfo, vers, backup, 1);
+
+ /* Is there a better term than "nonmergeable file"? What we
+ really mean is, not something that CVS cannot or does not
+ want to merge (there might be an external manual or
+ automatic merge process). */
+ error (0, 0, "nonmergeable file needs merge");
+ error (0, 0, "revision %s from repository is now in %s",
+ vers->vn_rcs, finfo->fullname);
+ error (0, 0, "file from working directory is now in %s", backup);
+ write_letter (finfo, 'C');
+
+ history_write ('C', finfo->update_dir, vers->vn_rcs, finfo->file,
+ finfo->repository);
+ retval = 0;
+ goto out;
+ }
+
+ status = RCS_merge (finfo->rcs, vers->srcfile->path, finfo->file,
+ vers->options, vers->vn_user, vers->vn_rcs);
+ if (status != 0 && status != 1)
+ {
+ error (0, status == -1 ? errno : 0,
+ "could not merge revision %s of %s", vers->vn_user, finfo->fullname);
+ error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s",
+ finfo->fullname, backup);
+ rename_file (backup, finfo->file);
+ retval = 1;
+ goto out;
+ }
+
+ if (strcmp (vers->options, "-V4") == 0)
+ vers->options[0] = '\0';
+
+ /* fix up the vers structure, in case it is used by join */
+ if (join_rev1)
+ {
+ /* FIXME: Throwing away the original revision info is almost
+ certainly wrong -- what if join_rev1 is "BASE"? */
+ if (vers->vn_user != NULL)
+ free (vers->vn_user);
+ vers->vn_user = xstrdup (vers->vn_rcs);
+ }
+
+ RegisterMerge (finfo, vers, backup, status);
+
+ if (status == 1)
+ {
+ error (0, 0, "conflicts found in %s", finfo->fullname);
+
+ write_letter (finfo, 'C');
+
+ history_write ('C', finfo->update_dir, vers->vn_rcs, finfo->file,
+ finfo->repository);
+
+ }
+ else /* status == 0 */
+ {
+ history_write ('G', finfo->update_dir, vers->vn_rcs, finfo->file,
+ finfo->repository);
+
+ /* FIXME: the noexec case is broken. RCS_merge could be doing the
+ xcmp on the temporary files without much hassle, I think. */
+ if (!noexec && !xcmp (backup, finfo->file))
+ {
+ cvs_output (finfo->fullname, 0);
+ cvs_output (" already contains the differences between ", 0);
+ cvs_output (vers->vn_user, 0);
+ cvs_output (" and ", 0);
+ cvs_output (vers->vn_rcs, 0);
+ cvs_output ("\n", 1);
+
+ retval = 0;
+ goto out;
+ }
+
+ write_letter (finfo, 'M');
+ }
+ retval = 0;
+ out:
+ free (backup);
+ return retval;
+}
+
+
+
+/*
+ * Do all the magic associated with a file which needs to be joined
+ * (reached via the -j option to checkout or update).
+ *
+ * INPUTS
+ * finfo File information about the destination file.
+ * vers The Vers_TS structure for finfo.
+ *
+ * GLOBALS
+ * join_rev1 From the command line.
+ * join_rev2 From the command line.
+ * server_active Natch.
+ *
+ * ASSUMPTIONS
+ * 1. Is not called in client mode.
+ */
+static void
+join_file (struct file_info *finfo, Vers_TS *vers)
+{
+ char *backup;
+ char *t_options;
+ int status;
+
+ char *rev1;
+ char *rev2;
+ char *jrev1;
+ char *jrev2;
+ char *jdate1;
+ char *jdate2;
+
+ TRACE (TRACE_FUNCTION, "join_file(%s, %s%s%s%s, %s, %s)",
+ finfo->file,
+ vers->tag ? vers->tag : "",
+ vers->tag ? " (" : "",
+ vers->vn_rcs ? vers->vn_rcs : "",
+ vers->tag ? ")" : "",
+ join_rev1 ? join_rev1 : "",
+ join_rev2 ? join_rev2 : "");
+
+ jrev1 = join_rev1;
+ jrev2 = join_rev2;
+ jdate1 = join_date1;
+ jdate2 = join_date2;
+
+ /* Determine if we need to do anything at all. */
+ if (vers->srcfile == NULL ||
+ vers->srcfile->path == NULL)
+ {
+ return;
+ }
+
+ /* If only one join revision is specified, it becomes the second
+ revision. */
+ if (jrev2 == NULL)
+ {
+ jrev2 = jrev1;
+ jrev1 = NULL;
+ jdate2 = jdate1;
+ jdate1 = NULL;
+ }
+
+ /* FIXME: Need to handle "BASE" for jrev1 and/or jrev2. Note caveat
+ below about vn_user. */
+
+ /* Convert the second revision, walking branches and dates. */
+ rev2 = RCS_getversion (vers->srcfile, jrev2, jdate2, 1, NULL);
+
+ /* If this is a merge of two revisions, get the first revision.
+ If only one join tag was specified, then the first revision is
+ the greatest common ancestor of the second revision and the
+ working file. */
+ if (jrev1 != NULL)
+ rev1 = RCS_getversion (vers->srcfile, jrev1, jdate1, 1, NULL);
+ else
+ {
+ /* Note that we use vn_rcs here, since vn_user may contain a
+ special string such as "-nn". */
+ if (vers->vn_rcs == NULL)
+ rev1 = NULL;
+ else if (rev2 == NULL)
+ {
+ /* This means that the file never existed on the branch.
+ It does not mean that the file was removed on the
+ branch: that case is represented by a dead rev2. If
+ the file never existed on the branch, then we have
+ nothing to merge, so we just return. */
+ return;
+ }
+ else
+ rev1 = gca (vers->vn_rcs, rev2);
+ }
+
+ /* Handle a nonexistent or dead merge target. */
+ if (rev2 == NULL || RCS_isdead (vers->srcfile, rev2))
+ {
+ char *mrev;
+
+ if (rev2 != NULL)
+ free (rev2);
+
+ /* If the first revision doesn't exist either, then there is
+ no change between the two revisions, so we don't do
+ anything. */
+ if (rev1 == NULL || RCS_isdead (vers->srcfile, rev1))
+ {
+ if (rev1 != NULL)
+ free (rev1);
+ return;
+ }
+
+ /* If we are merging two revisions, then the file was removed
+ between the first revision and the second one. In this
+ case we want to mark the file for removal.
+
+ If we are merging one revision, then the file has been
+ removed between the greatest common ancestor and the merge
+ revision. From the perspective of the branch on to which
+ we ar emerging, which may be the trunk, either 1) the file
+ does not currently exist on the target, or 2) the file has
+ not been modified on the target branch since the greatest
+ common ancestor, or 3) the file has been modified on the
+ target branch since the greatest common ancestor. In case
+ 1 there is nothing to do. In case 2 we mark the file for
+ removal. In case 3 we have a conflict.
+
+ Note that the handling is slightly different depending upon
+ whether one or two join targets were specified. If two
+ join targets were specified, we don't check whether the
+ file was modified since a given point. My reasoning is
+ that if you ask for an explicit merge between two tags,
+ then you want to merge in whatever was changed between
+ those two tags. If a file was removed between the two
+ tags, then you want it to be removed. However, if you ask
+ for a merge of a branch, then you want to merge in all
+ changes which were made on the branch. If a file was
+ removed on the branch, that is a change to the file. If
+ the file was also changed on the main line, then that is
+ also a change. These two changes--the file removal and the
+ modification--must be merged. This is a conflict. */
+
+ /* If the user file is dead, or does not exist, or has been
+ marked for removal, then there is nothing to do. */
+ if (vers->vn_user == NULL
+ || vers->vn_user[0] == '-'
+ || RCS_isdead (vers->srcfile, vers->vn_user))
+ {
+ if (rev1 != NULL)
+ free (rev1);
+ return;
+ }
+
+ /* If the user file has been marked for addition, or has been
+ locally modified, then we have a conflict which we can not
+ resolve. No_Difference will already have been called in
+ this case, so comparing the timestamps is sufficient to
+ determine whether the file is locally modified. */
+ if (strcmp (vers->vn_user, "0") == 0
+ || (vers->ts_user != NULL
+ && strcmp (vers->ts_user, vers->ts_rcs) != 0))
+ {
+ if (jdate2 != NULL)
+ error (0, 0,
+ "file %s is locally modified, but has been removed in revision %s as of %s",
+ finfo->fullname, jrev2, jdate2);
+ else
+ error (0, 0,
+ "file %s is locally modified, but has been removed in revision %s",
+ finfo->fullname, jrev2);
+
+ /* FIXME: Should we arrange to return a non-zero exit
+ status? */
+
+ if (rev1 != NULL)
+ free (rev1);
+
+ return;
+ }
+
+ /* If only one join tag was specified, and the user file has
+ been changed since the greatest common ancestor (rev1),
+ then there is a conflict we can not resolve. See above for
+ the rationale. */
+ if (join_rev2 == NULL
+ && strcmp (rev1, vers->vn_user) != 0)
+ {
+ if (jdate2 != NULL)
+ error (0, 0,
+ "file %s has been modified, but has been removed in revision %s as of %s",
+ finfo->fullname, jrev2, jdate2);
+ else
+ error (0, 0,
+ "file %s has been modified, but has been removed in revision %s",
+ finfo->fullname, jrev2);
+
+ /* FIXME: Should we arrange to return a non-zero exit
+ status? */
+
+ if (rev1 != NULL)
+ free (rev1);
+
+ return;
+ }
+
+ if (rev1 != NULL)
+ free (rev1);
+
+ /* The user file exists and has not been modified. Mark it
+ for removal. FIXME: If we are doing a checkout, this has
+ the effect of first checking out the file, and then
+ removing it. It would be better to just register the
+ removal.
+
+ The same goes for a removal then an add. e.g.
+ cvs up -rbr -jbr2 could remove and readd the same file
+ */
+ /* save the rev since server_updated might invalidate it */
+ mrev = Xasprintf ("-%s", vers->vn_user);
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ {
+ server_scratch (finfo->file);
+ server_updated (finfo, vers, SERVER_UPDATED, (mode_t) -1,
+ NULL, NULL);
+ }
+#endif
+ Register (finfo->entries, finfo->file, mrev, vers->ts_rcs,
+ vers->options, vers->tag, vers->date, vers->ts_conflict);
+ free (mrev);
+ /* We need to check existence_error here because if we are
+ running as the server, and the file is up to date in the
+ working directory, the client will not have sent us a copy. */
+ if (unlink_file (finfo->file) < 0 && ! existence_error (errno))
+ error (0, errno, "cannot remove file %s", finfo->fullname);
+#ifdef SERVER_SUPPORT
+ if (server_active)
+ server_checked_in (finfo->file, finfo->update_dir,
+ finfo->repository);
+#endif
+ if (! really_quiet)
+ error (0, 0, "scheduling `%s' for removal", finfo->fullname);
+
+ return;
+ }
+
+ /* If the two merge revisions are the same, then there is nothing
+ * to do. This needs to be checked before the rev2 == up-to-date base
+ * revision check tha comes next. Otherwise, rev1 can == rev2 and get an
+ * "already contains the changes between <rev1> and <rev1>" message.
+ */
+ if (rev1 && strcmp (rev1, rev2) == 0)
+ {
+ free (rev1);
+ free (rev2);
+ return;
+ }
+
+ /* If we know that the user file is up-to-date, then it becomes an
+ * optimization to skip the merge when rev2 is the same as the base
+ * revision. i.e. we know that diff3(file2,file1,file2) will produce
+ * file2.
+ */
+ if (vers->vn_user != NULL && vers->ts_user != NULL
+ && strcmp (vers->ts_user, vers->ts_rcs) == 0
+ && strcmp (rev2, vers->vn_user) == 0)
+ {
+ if (!really_quiet)
+ {
+ cvs_output (finfo->fullname, 0);
+ cvs_output (" already contains the differences between ", 0);
+ cvs_output (rev1 ? rev1 : "creation", 0);
+ cvs_output (" and ", 0);
+ cvs_output (rev2, 0);
+ cvs_output ("\n", 1);
+ }
+
+ if (rev1 != NULL)
+ free (rev1);
+ free (rev2);
+
+ return;
+ }
+
+ /* If rev1 is dead or does not exist, then the file was added
+ between rev1 and rev2. */
+ if (rev1 == NULL || RCS_isdead (vers->srcfile, rev1))
+ {
+ if (rev1 != NULL)
+ free (rev1);
+ free (rev2);
+
+ /* If the file does not exist in the working directory, then
+ we can just check out the new revision and mark it for
+ addition. */
+ if (vers->vn_user == NULL)
+ {
+ char *saved_options = options;
+ Vers_TS *xvers;
+
+ xvers = Version_TS (finfo, vers->options, jrev2, jdate2, 1, 0);
+
+ /* Reset any keyword expansion option. Otherwise, when a
+ command like `cvs update -kk -jT1 -jT2' creates a new file
+ (because a file had the T2 tag, but not T1), the subsequent
+ commit of that just-added file effectively would set the
+ admin `-kk' option for that file in the repository. */
+ options = NULL;
+
+ /* FIXME: If checkout_file fails, we should arrange to
+ return a non-zero exit status. */
+ status = checkout_file (finfo, xvers, 1, 0, 1);
+ options = saved_options;
+
+ freevers_ts (&xvers);
+
+ return;
+ }
+
+ /* The file currently exists in the working directory, so we
+ have a conflict which we can not resolve. Note that this
+ is true even if the file is marked for addition or removal. */
+
+ if (jdate2 != NULL)
+ error (0, 0,
+ "file %s exists, but has been added in revision %s as of %s",
+ finfo->fullname, jrev2, jdate2);
+ else
+ error (0, 0,
+ "file %s exists, but has been added in revision %s",
+ finfo->fullname, jrev2);
+
+ return;
+ }
+
+ /* If there is no working file, then we can't do the merge. */
+ if (vers->vn_user == NULL || vers->vn_user[0] == '-')
+ {
+ free (rev1);
+ free (rev2);
+
+ if (jdate2 != NULL)
+ error (0, 0,
+ "file %s does not exist, but is present in revision %s as of %s",
+ finfo->fullname, jrev2, jdate2);
+ else
+ error (0, 0,
+ "file %s does not exist, but is present in revision %s",
+ finfo->fullname, jrev2);
+
+ /* FIXME: Should we arrange to return a non-zero exit status? */
+
+ return;
+ }
+
+#ifdef SERVER_SUPPORT
+ if (server_active && !isreadable (finfo->file))
+ {
+ int retcode;
+ /* The file is up to date. Need to check out the current contents. */
+ /* FIXME - see the FIXME comment above the call to RCS_checkout in the
+ * patch_file function.
+ */
+ retcode = RCS_checkout (vers->srcfile, finfo->file,
+ vers->vn_user, vers->tag,
+ NULL, RUN_TTY, NULL, NULL);
+ if (retcode != 0)
+ error (1, 0,
+ "failed to check out %s file", finfo->fullname);
+ }
+#endif
+
+ /*
+ * The users currently modified file is moved to a backup file name
+ * ".#filename.version", so that it will stay around for a few days
+ * before being automatically removed by some cron daemon. The "version"
+ * is the version of the file that the user was most up-to-date with
+ * before the merge.
+ */
+ backup = Xasprintf ("%s%s.%s", BAKPREFIX, finfo->file, vers->vn_user);
+
+ if (unlink_file (backup) < 0
+ && !existence_error (errno))
+ error (0, errno, "cannot remove %s", backup);
+ copy_file (finfo->file, backup);
+ xchmod (finfo->file, 1);
+
+ t_options = vers->options;
+#if 0
+ if (*t_options == '\0')
+ t_options = "-kk"; /* to ignore keyword expansions */
+#endif
+
+ /* If the source of the merge is the same as the working file
+ revision, then we can just RCS_checkout the target (no merging
+ as such). In the text file case, this is probably quite
+ similar to the RCS_merge, but in the binary file case,
+ RCS_merge gives all kinds of trouble. */
+ if (vers->vn_user != NULL
+ && strcmp (rev1, vers->vn_user) == 0
+ /* See comments above about how No_Difference has already been
+ called. */
+ && vers->ts_user != NULL
+ && strcmp (vers->ts_user, vers->ts_rcs) == 0
+
+ /* Avoid this in the text file case. See below for why.
+ */
+ && (strcmp (t_options, "-kb") == 0
+ || wrap_merge_is_copy (finfo->file)))
+ {
+ /* FIXME: Verify my comment below:
+ *
+ * RCS_merge does nothing with keywords. It merges the changes between
+ * two revisions without expanding the keywords (it might expand in
+ * -kk mode before computing the diff between rev1 and rev2 - I'm not
+ * sure). In other words, the keyword lines in the current work file
+ * get left alone.
+ *
+ * Therfore, checking out the destination revision (rev2) is probably
+ * incorrect in the text case since we should see the keywords that were
+ * substituted into the original file at the time it was checked out
+ * and not the keywords from rev2.
+ *
+ * Also, it is safe to pass in NULL for nametag since we know no
+ * substitution is happening during the binary mode checkout.
+ */
+ if (RCS_checkout (finfo->rcs, finfo->file, rev2, NULL, t_options,
+ RUN_TTY, NULL, NULL) != 0)
+ status = 2;
+ else
+ status = 0;
+
+ /* OK, this is really stupid. RCS_checkout carefully removes
+ write permissions, and we carefully put them back. But
+ until someone gets around to fixing it, that seems like the
+ easiest way to get what would seem to be the right mode.
+ I don't check CVSWRITE or _watched; I haven't thought about
+ that in great detail, but it seems like a watched file should
+ be checked out (writable) after a merge. */
+ xchmod (finfo->file, 1);
+
+ /* Traditionally, the text file case prints a whole bunch of
+ scary looking and verbose output which fails to tell the user
+ what is really going on (it gives them rev1 and rev2 but doesn't
+ indicate in any way that rev1 == vn_user). I think just a
+ simple "U foo" is good here; it seems analogous to the case in
+ which the file was added on the branch in terms of what to
+ print. */
+ write_letter (finfo, 'U');
+ }
+ else if (strcmp (t_options, "-kb") == 0
+ || wrap_merge_is_copy (finfo->file)
+ || special_file_mismatch (finfo, rev1, rev2))
+ {
+ /* We are dealing with binary files, or files with a
+ permission/linkage mismatch (this second case only occurs when
+ PRESERVE_PERMISSIONS_SUPPORT is enabled), and real merging would
+ need to take place. This is a conflict. We give the user
+ the two files, and let them resolve it. It is possible
+ that we should require a "touch foo" or similar step before
+ we allow a checkin. */
+ if (RCS_checkout (finfo->rcs, finfo->file, rev2, NULL,
+ t_options, RUN_TTY, NULL, NULL) != 0)
+ status = 2;
+ else
+ status = 0;
+
+ /* OK, this is really stupid. RCS_checkout carefully removes
+ write permissions, and we carefully put them back. But
+ until someone gets around to fixing it, that seems like the
+ easiest way to get what would seem to be the right mode.
+ I don't check CVSWRITE or _watched; I haven't thought about
+ that in great detail, but it seems like a watched file should
+ be checked out (writable) after a merge. */
+ xchmod (finfo->file, 1);
+
+ /* Hmm. We don't give them REV1 anywhere. I guess most people
+ probably don't have a 3-way merge tool for the file type in
+ question, and might just get confused if we tried to either
+ provide them with a copy of the file from REV1, or even just
+ told them what REV1 is so they can get it themself, but it
+ might be worth thinking about. */
+ /* See comment in merge_file about the "nonmergeable file"
+ terminology. */
+ error (0, 0, "nonmergeable file needs merge");
+ error (0, 0, "revision %s from repository is now in %s",
+ rev2, finfo->fullname);
+ error (0, 0, "file from working directory is now in %s", backup);
+ write_letter (finfo, 'C');
+ }
+ else
+ status = RCS_merge (finfo->rcs, vers->srcfile->path, finfo->file,
+ t_options, rev1, rev2);
+
+ if (status != 0)
+ {
+ if (status != 1)
+ {
+ error (0, status == -1 ? errno : 0,
+ "could not merge revision %s of %s", rev2, finfo->fullname);
+ error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s",
+ finfo->fullname, backup);
+ rename_file (backup, finfo->file);
+ }
+ }
+ else /* status == 0 */
+ {
+ /* FIXME: the noexec case is broken. RCS_merge could be doing the
+ xcmp on the temporary files without much hassle, I think. */
+ if (!noexec && !xcmp (backup, finfo->file))
+ {
+ if (!really_quiet)
+ {
+ cvs_output (finfo->fullname, 0);
+ cvs_output (" already contains the differences between ", 0);
+ cvs_output (rev1, 0);
+ cvs_output (" and ", 0);
+ cvs_output (rev2, 0);
+ cvs_output ("\n", 1);
+ }
+
+ /* and skip the registering and sending the new file since it
+ * hasn't been updated.
+ */
+ goto out;
+ }
+ }
+
+ /* The file has changed, but if we just checked it out it may
+ still have the same timestamp it did when it was first
+ registered above in checkout_file. We register it again with a
+ dummy timestamp to make sure that later runs of CVS will
+ recognize that it has changed.
+
+ We don't actually need to register again if we called
+ RCS_checkout above, and we aren't running as the server.
+ However, that is not the normal case, and calling Register
+ again won't cost much in that case. */
+ RegisterMerge (finfo, vers, backup, status);
+
+out:
+ free (rev1);
+ free (rev2);
+ free (backup);
+}
+
+
+
+/*
+ * Report whether revisions REV1 and REV2 of FINFO agree on:
+ * . file ownership
+ * . permissions
+ * . major and minor device numbers
+ * . symbolic links
+ * . hard links
+ *
+ * If either REV1 or REV2 is NULL, the working copy is used instead.
+ *
+ * Return 1 if the files differ on these data.
+ */
+
+int
+special_file_mismatch (struct file_info *finfo, char *rev1, char *rev2)
+{
+#ifdef PRESERVE_PERMISSIONS_SUPPORT
+ struct stat sb;
+ RCSVers *vp;
+ Node *n;
+ uid_t rev1_uid, rev2_uid;
+ gid_t rev1_gid, rev2_gid;
+ mode_t rev1_mode, rev2_mode;
+ unsigned long dev_long;
+ dev_t rev1_dev, rev2_dev;
+ char *rev1_symlink = NULL;
+ char *rev2_symlink = NULL;
+ List *rev1_hardlinks = NULL;
+ List *rev2_hardlinks = NULL;
+ int check_uids, check_gids, check_modes;
+ int result;
+
+ /* If we don't care about special file info, then
+ don't report a mismatch in any case. */
+ if (!preserve_perms)
+ return 0;
+
+ /* When special_file_mismatch is called from No_Difference, the
+ RCS file has been only partially parsed. We must read the
+ delta tree in order to compare special file info recorded in
+ the delta nodes. (I think this is safe. -twp) */
+ if (finfo->rcs->flags & PARTIAL)
+ RCS_reparsercsfile (finfo->rcs, NULL, NULL);
+
+ check_uids = check_gids = check_modes = 1;
+
+ /* Obtain file information for REV1. If this is null, then stat
+ finfo->file and use that info. */
+ /* If a revision does not know anything about its status,
+ then presumably it doesn't matter, and indicates no conflict. */
+
+ if (rev1 == NULL)
+ {
+ ssize_t rsize;
+
+ if ((rsize = islink (finfo->file)) > 0)
+ rev1_symlink = Xreadlink (finfo->file, rsize);
+ else
+ {
+# ifdef HAVE_STRUCT_STAT_ST_RDEV
+ if (lstat (finfo->file, &sb) < 0)
+ error (1, errno, "could not get file information for %s",
+ finfo->file);
+ rev1_uid = sb.st_uid;
+ rev1_gid = sb.st_gid;
+ rev1_mode = sb.st_mode;
+ if (S_ISBLK (rev1_mode) || S_ISCHR (rev1_mode))
+ rev1_dev = sb.st_rdev;
+# else
+ error (1, 0, "cannot handle device files on this system (%s)",
+ finfo->file);
+# endif
+ }
+ rev1_hardlinks = list_linked_files_on_disk (finfo->file);
+ }
+ else
+ {
+ n = findnode (finfo->rcs->versions, rev1);
+ vp = n->data;
+
+ n = findnode (vp->other_delta, "symlink");
+ if (n != NULL)
+ rev1_symlink = xstrdup (n->data);
+ else
+ {
+ n = findnode (vp->other_delta, "owner");
+ if (n == NULL)
+ check_uids = 0; /* don't care */
+ else
+ rev1_uid = strtoul (n->data, NULL, 10);
+
+ n = findnode (vp->other_delta, "group");
+ if (n == NULL)
+ check_gids = 0; /* don't care */
+ else
+ rev1_gid = strtoul (n->data, NULL, 10);
+
+ n = findnode (vp->other_delta, "permissions");
+ if (n == NULL)
+ check_modes = 0; /* don't care */
+ else
+ rev1_mode = strtoul (n->data, NULL, 8);
+
+ n = findnode (vp->other_delta, "special");
+ if (n == NULL)
+ rev1_mode |= S_IFREG;
+ else
+ {
+ /* If the size of `ftype' changes, fix the sscanf call also */
+ char ftype[16];
+ if (sscanf (n->data, "%15s %lu", ftype,
+ &dev_long) < 2)
+ error (1, 0, "%s:%s has bad `special' newphrase %s",
+ finfo->file, rev1, (char *)n->data);
+ rev1_dev = dev_long;
+ if (strcmp (ftype, "character") == 0)
+ rev1_mode |= S_IFCHR;
+ else if (strcmp (ftype, "block") == 0)
+ rev1_mode |= S_IFBLK;
+ else
+ error (0, 0, "%s:%s unknown file type `%s'",
+ finfo->file, rev1, ftype);
+ }
+
+ rev1_hardlinks = vp->hardlinks;
+ if (rev1_hardlinks == NULL)
+ rev1_hardlinks = getlist();
+ }
+ }
+
+ /* Obtain file information for REV2. */
+ if (rev2 == NULL)
+ {
+ ssize_t rsize;
+
+ if ((rsize = islink (finfo->file)) > 0)
+ rev2_symlink = Xreadlink (finfo->file, rsize);
+ else
+ {
+# ifdef HAVE_STRUCT_STAT_ST_RDEV
+ if (lstat (finfo->file, &sb) < 0)
+ error (1, errno, "could not get file information for %s",
+ finfo->file);
+ rev2_uid = sb.st_uid;
+ rev2_gid = sb.st_gid;
+ rev2_mode = sb.st_mode;
+ if (S_ISBLK (rev2_mode) || S_ISCHR (rev2_mode))
+ rev2_dev = sb.st_rdev;
+# else
+ error (1, 0, "cannot handle device files on this system (%s)",
+ finfo->file);
+# endif
+ }
+ rev2_hardlinks = list_linked_files_on_disk (finfo->file);
+ }
+ else
+ {
+ n = findnode (finfo->rcs->versions, rev2);
+ vp = n->data;
+
+ n = findnode (vp->other_delta, "symlink");
+ if (n != NULL)
+ rev2_symlink = xstrdup (n->data);
+ else
+ {
+ n = findnode (vp->other_delta, "owner");
+ if (n == NULL)
+ check_uids = 0; /* don't care */
+ else
+ rev2_uid = strtoul (n->data, NULL, 10);
+
+ n = findnode (vp->other_delta, "group");
+ if (n == NULL)
+ check_gids = 0; /* don't care */
+ else
+ rev2_gid = strtoul (n->data, NULL, 10);
+
+ n = findnode (vp->other_delta, "permissions");
+ if (n == NULL)
+ check_modes = 0; /* don't care */
+ else
+ rev2_mode = strtoul (n->data, NULL, 8);
+
+ n = findnode (vp->other_delta, "special");
+ if (n == NULL)
+ rev2_mode |= S_IFREG;
+ else
+ {
+ /* If the size of `ftype' changes, fix the sscanf call also */
+ char ftype[16];
+ if (sscanf (n->data, "%15s %lu", ftype,
+ &dev_long) < 2)
+ error (1, 0, "%s:%s has bad `special' newphrase %s",
+ finfo->file, rev2, (char *)n->data);
+ rev2_dev = dev_long;
+ if (strcmp (ftype, "character") == 0)
+ rev2_mode |= S_IFCHR;
+ else if (strcmp (ftype, "block") == 0)
+ rev2_mode |= S_IFBLK;
+ else
+ error (0, 0, "%s:%s unknown file type `%s'",
+ finfo->file, rev2, ftype);
+ }
+
+ rev2_hardlinks = vp->hardlinks;
+ if (rev2_hardlinks == NULL)
+ rev2_hardlinks = getlist();
+ }
+ }
+
+ /* Check the user/group ownerships and file permissions, printing
+ an error for each mismatch found. Return 0 if all characteristics
+ matched, and 1 otherwise. */
+
+ result = 0;
+
+ /* Compare symlinks first, since symlinks are simpler (don't have
+ any other characteristics). */
+ if (rev1_symlink != NULL && rev2_symlink == NULL)
+ {
+ error (0, 0, "%s is a symbolic link",
+ (rev1 == NULL ? "working file" : rev1));
+ result = 1;
+ }
+ else if (rev1_symlink == NULL && rev2_symlink != NULL)
+ {
+ error (0, 0, "%s is a symbolic link",
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+ else if (rev1_symlink != NULL)
+ result = (strcmp (rev1_symlink, rev2_symlink) == 0);
+ else
+ {
+ /* Compare user ownership. */
+ if (check_uids && rev1_uid != rev2_uid)
+ {
+ error (0, 0, "%s: owner mismatch between %s and %s",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+
+ /* Compare group ownership. */
+ if (check_gids && rev1_gid != rev2_gid)
+ {
+ error (0, 0, "%s: group mismatch between %s and %s",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+
+ /* Compare permissions. */
+ if (check_modes &&
+ (rev1_mode & 07777) != (rev2_mode & 07777))
+ {
+ error (0, 0, "%s: permission mismatch between %s and %s",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+
+ /* Compare device file characteristics. */
+ if ((rev1_mode & S_IFMT) != (rev2_mode & S_IFMT))
+ {
+ error (0, 0, "%s: %s and %s are different file types",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+ else if (S_ISBLK (rev1_mode))
+ {
+ if (rev1_dev != rev2_dev)
+ {
+ error (0, 0, "%s: device numbers of %s and %s do not match",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+ }
+
+ /* Compare hard links. */
+ if (compare_linkage_lists (rev1_hardlinks, rev2_hardlinks) == 0)
+ {
+ error (0, 0, "%s: hard linkage of %s and %s do not match",
+ finfo->file,
+ (rev1 == NULL ? "working file" : rev1),
+ (rev2 == NULL ? "working file" : rev2));
+ result = 1;
+ }
+ }
+
+ if (rev1_symlink != NULL)
+ free (rev1_symlink);
+ if (rev2_symlink != NULL)
+ free (rev2_symlink);
+ if (rev1_hardlinks != NULL)
+ dellist (&rev1_hardlinks);
+ if (rev2_hardlinks != NULL)
+ dellist (&rev2_hardlinks);
+
+ return result;
+#else
+ return 0;
+#endif
+}
+
+
+
+int
+joining (void)
+{
+ return join_rev1 || join_date1;
+}