diff options
Diffstat (limited to 'contrib/cvs/src/update.c')
-rw-r--r-- | contrib/cvs/src/update.c | 1830 |
1 files changed, 1830 insertions, 0 deletions
diff --git a/contrib/cvs/src/update.c b/contrib/cvs/src/update.c new file mode 100644 index 0000000000000..247831627947f --- /dev/null +++ b/contrib/cvs/src/update.c @@ -0,0 +1,1830 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * 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 1.4 kit. + * + * "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" +#ifdef SERVER_SUPPORT +#include "md5.h" +#endif +#include "watch.h" +#include "fileattr.h" +#include "edit.h" + +static int checkout_file PROTO((char *file, char *repository, List *entries, + RCSNode *rcsnode, Vers_TS *vers_ts, char *update_dir)); +#ifdef SERVER_SUPPORT +static int patch_file PROTO((char *file, char *repository, List *entries, + RCSNode*rcsnode, Vers_TS *vers_ts, char *update_dir, + int *docheckout, struct stat *file_info, + unsigned char *checksum)); +#endif +static int isemptydir PROTO((char *dir)); +static int merge_file PROTO((char *file, char *repository, List *entries, + Vers_TS *vers, char *update_dir)); +static int scratch_file PROTO((char *file, char *repository, List * entries, + char *update_dir)); +static Dtype update_dirent_proc PROTO((char *dir, char *repository, char *update_dir)); +static int update_dirleave_proc PROTO((char *dir, int err, char *update_dir)); +static int update_fileproc PROTO ((struct file_info *)); +static int update_filesdone_proc PROTO((int err, char *repository, + char *update_dir)); +static int write_letter PROTO((char *file, int letter, char *update_dir)); +#ifdef SERVER_SUPPORT +static void join_file PROTO((char *file, RCSNode *rcsnode, Vers_TS *vers_ts, + char *update_dir, List *entries, char *repository)); +#else +static void join_file PROTO((char *file, RCSNode *rcsnode, Vers_TS *vers_ts, + char *update_dir, List *entries)); +#endif + +static char *options = NULL; +static char *tag = NULL; +static char *date = NULL; +static char *join_rev1, *date_rev1; +static char *join_rev2, *date_rev2; +static int aflag = 0; +static int force_tag_match = 1; +static int update_build_dirs = 0; +static int update_prune_dirs = 0; +static int pipeout = 0; +#ifdef SERVER_SUPPORT +static int patches = 0; +#endif +static List *ignlist = (List *) NULL; +static time_t last_register_time; +static const char *const update_usage[] = +{ + "Usage: %s %s [-APdflRp] [-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-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.\n", + "\t-k kopt\tUse RCS kopt -k option on checkout.\n", + "\t-r rev\tUpdate using specified revision/tag.\n", + "\t-D date\tSet date to update from.\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", + NULL +}; + +/* + * update is the argv,argc based front end for arg parsing + */ +int +update (argc, argv) + int argc; + char **argv; +{ + int c, err; + int local = 0; /* recursive by default */ + int which; /* where to look for files and dirs */ + + if (argc == -1) + usage (update_usage); + + ign_setup (); + wrap_setup (); + + /* parse the args */ + optind = 1; + while ((c = getopt (argc, argv, "ApPflRQqduk:r:D:j:I:W:")) != -1) + { + switch (c) + { + case 'A': + aflag = 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': +#ifdef SERVER_SUPPORT + /* The CVS 1.5 client sends these options (in addition to + Global_option requests), so we must ignore them. */ + if (!server_active) +#endif + error (1, 0, + "-q or -Q must be specified before \"%s\"", + command_name); + break; + case 'd': + update_build_dirs = 1; + break; + case 'f': + force_tag_match = 0; + break; + case 'r': + tag = optarg; + break; + case 'D': + 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_rev2) + error (1, 0, "only two -j options can be specified"); + if (join_rev1) + join_rev2 = optarg; + else + join_rev1 = optarg; + break; + case 'u': +#ifdef SERVER_SUPPORT + if (server_active) + patches = 1; + else +#endif + usage (update_usage); + break; + case '?': + default: + usage (update_usage); + break; + } + } + argc -= optind; + argv += optind; + +#ifdef CLIENT_SUPPORT + if (client_active) + { + /* 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. */ + 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 (update_prune_dirs) + send_arg("-P"); + client_prune_dirs = update_prune_dirs; + option_with_arg ("-r", tag); + if (date) + client_senddate (date); + if (join_rev1) + option_with_arg ("-j", join_rev1); + if (join_rev2) + option_with_arg ("-j", join_rev2); + + /* 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. */ + if (failed_patches == NULL) + { + struct request *rq; + + for (rq = requests; rq->name != NULL; rq++) + { + if (strcmp (rq->name, "update-patches") == 0) + { + if (rq->status == rq_supported) + { + send_arg("-u"); + } + break; + } + } + } + + if (failed_patches == NULL) + { + send_file_names (argc, argv, SEND_EXPAND_WILD); + send_files (argc, argv, local, aflag); + } + else + { + int i; + + (void) printf ("%s client: refetching unpatchable files\n", + program_name); + + if (toplevel_wd[0] != '\0' + && chdir (toplevel_wd) < 0) + { + error (1, errno, "could not chdir to %s", toplevel_wd); + } + + for (i = 0; i < failed_patches_count; i++) + (void) unlink_file (failed_patches[i]); + send_file_names (failed_patches_count, failed_patches, 0); + send_files (failed_patches_count, failed_patches, local, + aflag); + } + + failed_patches = NULL; + failed_patches_count = 0; + + send_to_server ("update\012", 0); + + status = get_responses_and_close (); + if (status != 0) + return status; + + } while (failed_patches != NULL); + + return 0; + } +#endif + + if (tag != NULL) + tag_check_valid (tag, argc, argv, local, aflag, ""); + /* FIXME: We don't call tag_check_valid on join_rev1 and join_rev2 + yet (make sure to handle ':' correctly if we do, though). */ + + /* + * 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) + server_clear_entstat (".", Name_Repository (NULL, NULL)); +#endif + } + + /* keep the CVS/Tag file current with the specified arguments */ + if (aflag || tag || date) + { + WriteTag ((char *) NULL, tag, date); +#ifdef SERVER_SUPPORT + if (server_active) + server_set_sticky (".", Name_Repository (NULL, NULL), tag, date); +#endif + } + } + + /* 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 != NULL || date != NULL || joining()) + 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, join_rev1, join_rev2, (char *) NULL); + + /* free the space Make_Date allocated if necessary */ + if (date != NULL) + free (date); + + return (err); +} + +/* + * Command line interface to update (used by checkout) + */ +int +do_update (argc, argv, xoptions, xtag, xdate, xforce, local, xbuild, xaflag, + xprune, xpipeout, which, xjoin_rev1, xjoin_rev2, preload_update_dir) + 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_rev2; + char *preload_update_dir; +{ + int err = 0; + char *cp; + + /* 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; + + /* setup the join support */ + join_rev1 = xjoin_rev1; + join_rev2 = xjoin_rev2; + if (join_rev1 && (cp = strchr (join_rev1, ':')) != NULL) + { + *cp++ = '\0'; + date_rev1 = Make_Date (cp); + } + else + date_rev1 = (char *) NULL; + if (join_rev2 && (cp = strchr (join_rev2, ':')) != NULL) + { + *cp++ = '\0'; + date_rev2 = Make_Date (cp); + } + else + date_rev2 = (char *) NULL; + + /* call the recursion processor */ + err = start_recursion (update_fileproc, update_filesdone_proc, + update_dirent_proc, update_dirleave_proc, + argc, argv, local, which, aflag, 1, + preload_update_dir, 1, 0); + + /* see if we need to sleep before returning */ + if (last_register_time) + { + time_t now; + + (void) time (&now); + if (now == last_register_time) + sleep (1); /* to avoid time-stamp races */ + } + + return (err); +} + +/* + * 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 (finfo) + struct file_info *finfo; +{ + int retval; + Ctype status; + Vers_TS *vers; + + status = Classify_File (finfo->file, tag, date, options, force_tag_match, + aflag, finfo->repository, finfo->entries, finfo->rcs, &vers, + finfo->update_dir, pipeout); + 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 */ +#ifdef SERVER_SUPPORT + case T_PATCH: /* needs patch */ +#endif + retval = checkout_file (finfo->file, finfo->repository, finfo->entries, finfo->rcs, + vers, finfo->update_dir); + 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; + (void) write_letter (finfo->file, 'C', finfo->update_dir); + break; + case T_NEEDS_MERGE: /* needs merging */ + if (noexec) + { + retval = 1; + (void) write_letter (finfo->file, 'C', finfo->update_dir); + } + else + { + if (wrap_merge_is_copy (finfo->file)) + /* Should we be warning the user that we are + * overwriting the user's copy of the file? */ + retval = checkout_file (finfo->file, finfo->repository, finfo->entries, + finfo->rcs, vers, finfo->update_dir); + else + retval = merge_file (finfo->file, finfo->repository, finfo->entries, + vers, finfo->update_dir); + } + break; + case T_MODIFIED: /* locally modified */ + retval = 0; + if (vers->ts_conflict) + { + char *filestamp; + int retcode; + + /* + * If the timestamp has changed and no conflict indicators + * are found, it isn't a 'C' any more. + */ +#ifdef SERVER_SUPPORT + if (server_active) + retcode = vers->ts_conflict[0] != '='; + else { + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); + } +#else + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); +#endif + + if (retcode) + { + /* + * If the timestamps differ, look for Conflict + * indicators to see if 'C' anyway. + */ + run_setup ("%s", GREP); + run_arg (RCS_MERGE_PAT); + run_arg (finfo->file); + retcode = run_exec (RUN_TTY, DEVNULL, + RUN_TTY,RUN_NORMAL); + if (retcode == -1) + { + error (1, errno, + "fork failed while examining conflict in `%s'", + finfo->fullname); + } + } + if (!retcode) + { + (void) write_letter (finfo->file, 'C', finfo->update_dir); + 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, (char *)0); + } + } + if (!retval) + retval = write_letter (finfo->file, 'M', finfo->update_dir); + break; +#ifdef SERVER_SUPPORT + case T_PATCH: /* needs patch */ + if (patches) + { + int docheckout; + struct stat file_info; + unsigned char checksum[16]; + + retval = patch_file (finfo->file, finfo->repository, finfo->entries, finfo->rcs, + vers, finfo->update_dir, &docheckout, + &file_info, checksum); + if (! docheckout) + { + if (server_active && retval == 0) + server_updated (finfo->file, finfo->update_dir, finfo->repository, + SERVER_PATCHED, &file_info, + checksum); + break; + } + } + /* Fall through. */ + /* If we're not running as a server, just check the + file out. It's simpler and faster than starting up + two new processes (diff and patch). */ + /* Fall through. */ +#endif + case T_CHECKOUT: /* needs checkout */ + retval = checkout_file (finfo->file, finfo->repository, finfo->entries, finfo->rcs, + vers, finfo->update_dir); +#ifdef SERVER_SUPPORT + if (server_active && retval == 0) + server_updated (finfo->file, finfo->update_dir, finfo->repository, + SERVER_UPDATED, (struct stat *) NULL, + (unsigned char *) NULL); +#endif + break; + case T_ADDED: /* added but not committed */ + retval = write_letter (finfo->file, 'A', finfo->update_dir); + break; + case T_REMOVED: /* removed but not committed */ + retval = write_letter (finfo->file, 'R', finfo->update_dir); + break; + case T_REMOVE_ENTRY: /* needs to be un-registered */ + retval = scratch_file (finfo->file, finfo->repository, finfo->entries, finfo->update_dir); +#ifdef SERVER_SUPPORT + if (server_active && retval == 0) + server_updated (finfo->file, finfo->update_dir, finfo->repository, + SERVER_UPDATED, (struct stat *) NULL, + (unsigned char *) NULL); +#endif + 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) +#ifdef SERVER_SUPPORT + join_file (finfo->file, finfo->rcs, vers, finfo->update_dir, finfo->entries, finfo->repository); +#else + join_file (finfo->file, finfo->rcs, vers, finfo->update_dir, finfo->entries); +#endif + + /* 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); + if (addnode (ignlist, p) != 0) + freenode (p); + } + + freevers_ts (&vers); + return (retval); +} + +static void update_ignproc PROTO ((char *, char *)); + +static void +update_ignproc (file, dir) + char *file; + char *dir; +{ + (void) write_letter (file, '?', dir); +} + +/* ARGSUSED */ +static int +update_filesdone_proc (err, repository, update_dir) + int err; + char *repository; + char *update_dir; +{ + /* if this directory has an ignore list, process it then free it */ + if (ignlist) + { + ignore_files (ignlist, update_dir, update_ignproc); + dellist (&ignlist); + } + + /* Clean up CVS admin dirs if we are export */ + if (strcmp (command_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); + } +#ifdef SERVER_SUPPORT + else if (!server_active && !pipeout) +#else + else if (!pipeout) +#endif /* SERVER_SUPPORT */ + { + /* If there is no CVS/Root file, add one */ + if (!isfile (CVSADM_ROOT)) + Create_Root( (char *) NULL, CVSroot ); + } + + 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 (dir, repository, update_dir) + char *dir; + char *repository; + char *update_dir; +{ + 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); + + if (noexec) + { + /* ignore the missing dir if -n is specified */ + error (0, 0, "New directory `%s' -- ignored", dir); + return (R_SKIP_ALL); + } + else + { + /* otherwise, create the dir and appropriate adm files */ + make_directory (dir); + Create_Admin (dir, update_dir, repository, tag, date); + } + } + /* 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 = xmalloc (strlen (dir) + 80); + strcpy (cvsadmdir, dir); + strcat (cvsadmdir, "/"); + strcat (cvsadmdir, 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[PATH_MAX]; + + (void) sprintf (tmp, "%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 + } + + /* keep the CVS/Tag file current with the specified arguments */ + if (aflag || tag || date) + { + WriteTag (dir, tag, date); +#ifdef SERVER_SUPPORT + if (server_active) + server_set_sticky (update_dir, repository, tag, date); +#endif + } + + /* 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 (dir, err, update_dir) + char *dir; + int err; + char *update_dir; +{ + FILE *fp; + + /* run the update_prog if there is one */ + if (err == 0 && !pipeout && !noexec && + (fp = fopen (CVSADM_UPROG, "r")) != NULL) + { + char *cp; + char *repository; + char line[MAXLINELEN]; + + repository = Name_Repository ((char *) NULL, update_dir); + if (fgets (line, sizeof (line), fp) != NULL) + { + if ((cp = strrchr (line, '\n')) != NULL) + *cp = '\0'; + run_setup ("%s %s", line, repository); + (void) printf ("%s %s: Executing '", program_name, command_name); + run_print (stdout); + (void) printf ("'\n"); + (void) run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL); + } + (void) fclose (fp); + free (repository); + } + + /* FIXME: chdir ("..") loses with symlinks. */ + /* Prune empty dirs on the way out - if necessary */ + (void) chdir (".."); + if (update_prune_dirs && isemptydir (dir)) + { + /* 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); + } + + return (err); +} + +/* + * Returns 1 if the argument directory is completely empty, other than the + * existence of the CVS directory entry. Zero otherwise. + */ +static int +isemptydir (dir) + char *dir; +{ + DIR *dirp; + struct dirent *dp; + + if ((dirp = opendir (dir)) == NULL) + { + error (0, 0, "cannot open directory %s for empty check", dir); + return (0); + } + while ((dp = readdir (dirp)) != NULL) + { + if (strcmp (dp->d_name, ".") != 0 && strcmp (dp->d_name, "..") != 0 && + strcmp (dp->d_name, CVSADM) != 0) + { + (void) closedir (dirp); + return (0); + } + } + (void) closedir (dirp); + return (1); +} + +/* + * scratch the Entries file entry associated with a file + */ +static int +scratch_file (file, repository, entries, update_dir) + char *file; + char *repository; + List *entries; + char *update_dir; +{ + history_write ('W', update_dir, "", file, repository); + Scratch_Entry (entries, file); + (void) unlink_file (file); + return (0); +} + +/* + * check out a file - essentially returns the result of the fork on "co". + */ +static int +checkout_file (file, repository, entries, rcsnode, vers_ts, update_dir) + char *file; + char *repository; + List *entries; + RCSNode *rcsnode; + Vers_TS *vers_ts; + char *update_dir; +{ + char backup[PATH_MAX]; + int set_time, retval = 0; + int retcode = 0; + int status; + int file_is_dead; + + /* don't screw with backup files if we're going to stdout */ + if (!pipeout) + { + (void) sprintf (backup, "%s/%s%s", CVSADM, CVSPREFIX, file); + if (isfile (file)) + rename_file (file, backup); + else + (void) unlink_file (backup); + } + + 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) + { + (void) fprintf (stderr, "\ +===================================================================\n"); + if (update_dir[0]) + (void) fprintf (stderr, "Checking out %s/%s\n", + update_dir, file); + else + (void) fprintf (stderr, "Checking out %s\n", file); + (void) fprintf (stderr, "RCS: %s\n", vers_ts->srcfile->path); + (void) fprintf (stderr, "VERS: %s\n", vers_ts->vn_rcs); + (void) fprintf (stderr, "***************\n"); + } + } + + status = RCS_checkout (vers_ts->srcfile->path, + pipeout ? NULL : file, vers_ts->vn_tag, + vers_ts->options, RUN_TTY, 0, 0); + } + if (file_is_dead || status == 0) + { + if (!pipeout) + { + Vers_TS *xvers_ts; + int resurrecting; + + resurrecting = 0; + + if (file_is_dead && joining()) + { + if (RCS_getversion (vers_ts->srcfile, join_rev1, + date_rev1, 1, 0) + || (join_rev2 != NULL && + RCS_getversion (vers_ts->srcfile, join_rev2, + date_rev2, 1, 0))) + { + /* when joining, we need to get dead files checked + out. Try harder. */ + /* I think that RCS_FLAGS_FORCE is here only because + passing -f to co used to enable checking out + a dead revision in the old version of death + support which used a hacked RCS instead of using + the RCS state. */ + retcode = RCS_checkout (vers_ts->srcfile->path, file, + vers_ts->vn_rcs, + vers_ts->options, RUN_TTY, + RCS_FLAGS_FORCE, 0); + if (retcode != 0) + { + error (retcode == -1 ? 1 : 0, + retcode == -1 ? errno : 0, + "could not check out %s", file); + (void) unlink_file (backup); + return (retcode); + } + file_is_dead = 0; + resurrecting = 1; + } + else + { + /* If the file is dead and does not contain either of + the join revisions, then we don't want to check it + out. */ + return 0; + } + } + + if (cvswrite == TRUE + && !file_is_dead + && !fileattr_get (file, "_watched")) + xchmod (file, 1); + + { + /* 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 (file, getcaller (), NULL); + + memset (&args, 0, sizeof args); + args.remove_temp = 1; + watch_modify_watchers (file, &args); + } + + /* set the time from the RCS file iff it was unknown before */ + if (vers_ts->vn_user == NULL || + strncmp (vers_ts->ts_rcs, "Initial", 7) == 0) + { + set_time = 1; + } + else + set_time = 0; + + wrap_fromcvs_process_file (file); + + xvers_ts = Version_TS (repository, options, tag, date, file, + force_tag_match, set_time, entries, rcsnode); + if (strcmp (xvers_ts->options, "-V4") == 0) + xvers_ts->options[0] = '\0'; + + (void) time (&last_register_time); + + if (file_is_dead) + { + if (xvers_ts->vn_user != NULL) + { + if (update_dir[0] == '\0') + error (0, 0, + "warning: %s is not (any longer) pertinent", + file); + else + error (0, 0, + "warning: %s/%s is not (any longer) pertinent", + update_dir, file); + } + Scratch_Entry (entries, file); + if (unlink_file (file) < 0 && ! existence_error (errno)) + { + if (update_dir[0] == '\0') + error (0, errno, "cannot remove %s", file); + else + error (0, errno, "cannot remove %s/%s", update_dir, + file); + } + } + else + Register (entries, file, + resurrecting ? "0" : xvers_ts->vn_rcs, + xvers_ts->ts_user, xvers_ts->options, + xvers_ts->tag, xvers_ts->date, + (char *)0); /* Clear conflict flag on fresh checkout */ + + /* fix up the vers structure, in case it is used by join */ + if (join_rev1) + { + 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 (command_name, "update") == 0) + history_write ('U', update_dir, xvers_ts->vn_rcs, file, + repository); + + freevers_ts (&xvers_ts); + + if (!really_quiet && !file_is_dead) + { + write_letter (file, 'U', update_dir); + } + } + } + else + { + int old_errno = errno; /* save errno value over the rename */ + + if (!pipeout && isfile (backup)) + rename_file (backup, file); + + error (retcode == -1 ? 1 : 0, retcode == -1 ? old_errno : 0, + "could not check out %s", file); + + retval = retcode; + } + + if (!pipeout) + (void) unlink_file (backup); + + return (retval); +} + +#ifdef SERVER_SUPPORT +/* Patch a file. Runs rcsdiff. 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 (file, repository, entries, rcsnode, vers_ts, update_dir, + docheckout, file_info, checksum) + char *file; + char *repository; + List *entries; + RCSNode *rcsnode; + Vers_TS *vers_ts; + char *update_dir; + int *docheckout; + struct stat *file_info; + unsigned char *checksum; +{ + char backup[PATH_MAX]; + char file1[PATH_MAX]; + char file2[PATH_MAX]; + int retval = 0; + int retcode = 0; + int fail; + FILE *e; + + *docheckout = 0; + + if (pipeout || joining ()) + { + *docheckout = 1; + return 0; + } + + (void) sprintf (backup, "%s/%s%s", CVSADM, CVSPREFIX, file); + if (isfile (file)) + rename_file (file, backup); + else + (void) unlink_file (backup); + + (void) sprintf (file1, "%s/%s%s-1", CVSADM, CVSPREFIX, file); + (void) sprintf (file2, "%s/%s%s-2", CVSADM, CVSPREFIX, 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. */ + if (noexec) + retcode = 0; + else + retcode = RCS_checkout (vers_ts->srcfile->path, NULL, + vers_ts->vn_user, + vers_ts->options, file1, 0, 0); + if (retcode != 0) + fail = 1; + else + { + e = fopen (file1, "r"); + if (e == NULL) + fail = 1; + else + { + if (fseek (e, (long) -1, SEEK_END) == 0 + && getc (e) != '\n') + { + fail = 1; + } + fclose (e); + } + } + + if (! fail) + { + /* Check it out into file, and then move to file2, so that we + can get the right modes into *FILE_INFO. We can't check it + out directly into file2 because co doesn't understand how + to do that. */ + retcode = RCS_checkout (vers_ts->srcfile->path, file, + vers_ts->vn_rcs, + vers_ts->options, RUN_TTY, 0, 0); + if (retcode != 0) + fail = 1; + else + { + if (!isreadable (file)) + { + /* File is dead. */ + fail = 1; + } + else + { + rename_file (file, file2); + if (cvswrite == TRUE + && !fileattr_get (file, "_watched")) + xchmod (file2, 1); + e = fopen (file2, "r"); + if (e == NULL) + fail = 1; + else + { + struct MD5Context context; + int nl; + unsigned char buf[8192]; + unsigned len; + + nl = 0; + + /* Compute the MD5 checksum and make sure there is + a trailing newline. */ + MD5Init (&context); + while ((len = fread (buf, 1, sizeof buf, e)) != 0) + { + nl = buf[len - 1] == '\n'; + MD5Update (&context, buf, len); + } + MD5Final (checksum, &context); + + if (ferror (e) || ! nl) + { + fail = 1; + } + + fclose (e); + } + } + } + } + + retcode = 0; + if (! fail) + { + /* FIXME: This whole thing with diff/patch is rather more + convoluted than necessary (lots of forks and execs, need to + worry about versions of diff and patch, etc.). Also, we + send context lines which aren't needed (in the rare case in + which the diff doesn't apply, the checksum would catches it). + Solution perhaps is to librarify the RCS routines which apply + deltas or something equivalent. */ + /* This is -c, not -u, because we have no way of knowing which + DIFF is in use. */ + run_setup ("%s -c %s %s", DIFF, file1, file2); + + /* A retcode of 0 means no differences. 1 means some differences. */ + if ((retcode = run_exec (RUN_TTY, file, RUN_TTY, RUN_NORMAL)) != 0 + && retcode != 1) + { + fail = 1; + } + else + { +#define BINARY "Binary" + char buf[sizeof BINARY]; + unsigned int c; + + /* Check the diff output to make sure patch will be handle it. */ + e = fopen (file, "r"); + if (e == NULL) + error (1, errno, "could not open diff output file %s", file); + 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; + + /* This stuff is just copied blindly from checkout_file. I + don't really know what it does. */ + xvers_ts = Version_TS (repository, options, tag, date, file, + force_tag_match, 0, entries, rcsnode); + if (strcmp (xvers_ts->options, "-V4") == 0) + xvers_ts->options[0] = '\0'; + + Register (entries, file, xvers_ts->vn_rcs, + xvers_ts->ts_user, xvers_ts->options, + xvers_ts->tag, xvers_ts->date, NULL); + + if (stat (file2, file_info) < 0) + error (1, errno, "could not stat %s", file2); + + /* If this is really Update and not Checkout, recode history */ + if (strcmp (command_name, "update") == 0) + history_write ('P', update_dir, xvers_ts->vn_rcs, file, + repository); + + freevers_ts (&xvers_ts); + + if (!really_quiet) + { + write_letter (file, 'P', update_dir); + } + } + else + { + int old_errno = errno; /* save errno value over the rename */ + + if (isfile (backup)) + rename_file (backup, file); + + if (retcode != 0 && retcode != 1) + error (retcode == -1 ? 1 : 0, retcode == -1 ? old_errno : 0, + "could not diff %s", file); + + *docheckout = 1; + retval = retcode; + } + + (void) unlink_file (backup); + (void) unlink_file (file1); + (void) unlink_file (file2); + + return (retval); +} +#endif + +/* + * Several of the types we process only print a bit of information consisting + * of a single letter and the name. + */ +static int +write_letter (file, letter, update_dir) + char *file; + int letter; + char *update_dir; +{ + if (!really_quiet) + { + char buf[2]; + buf[0] = letter; + buf[1] = ' '; + cvs_output (buf, 2); + if (update_dir[0]) + { + cvs_output (update_dir, 0); + cvs_output ("/", 1); + } + cvs_output (file, 0); + cvs_output ("\n", 1); + } + return (0); +} + +/* + * Do all the magic associated with a file which needs to be merged + */ +static int +merge_file (file, repository, entries, vers, update_dir) + char *file; + char *repository; + List *entries; + Vers_TS *vers; + char *update_dir; +{ + char user[PATH_MAX]; + char backup[PATH_MAX]; + int status; + int retcode = 0; + + /* + * 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. + */ + (void) sprintf (backup, "%s%s.%s", BAKPREFIX, file, vers->vn_user); + if (update_dir[0]) + (void) sprintf (user, "%s/%s", update_dir, file); + else + (void) strcpy (user, file); + + (void) unlink_file (backup); + copy_file (file, backup); + xchmod (file, 1); + + status = RCS_merge(vers->srcfile->path, + 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, user); + error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s", + user, backup); + rename_file (backup, file); + return (1); + } + + if (strcmp (vers->options, "-V4") == 0) + vers->options[0] = '\0'; + (void) time (&last_register_time); + { + char *cp = 0; + + if (status) + cp = time_stamp (file); + Register (entries, file, vers->vn_rcs, vers->ts_rcs, vers->options, + vers->tag, vers->date, cp); + if (cp) + free (cp); + } + + /* fix up the vers structure, in case it is used by join */ + if (join_rev1) + { + if (vers->vn_user != NULL) + free (vers->vn_user); + vers->vn_user = xstrdup (vers->vn_rcs); + } + +#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 (file, update_dir, repository, backup); + server_updated (file, update_dir, repository, SERVER_MERGED, + (struct stat *) NULL, (unsigned char *) NULL); + } +#endif + + if (!noexec && !xcmp (backup, file)) + { + printf ("%s already contains the differences between %s and %s\n", + user, vers->vn_user, vers->vn_rcs); + history_write ('G', update_dir, vers->vn_rcs, file, repository); + return (0); + } + + if (status == 1) + { + if (!noexec) + error (0, 0, "conflicts found in %s", user); + + write_letter (file, 'C', update_dir); + + history_write ('C', update_dir, vers->vn_rcs, file, repository); + + } + else if (retcode == -1) + { + error (1, errno, "fork failed while examining update of %s", user); + } + else + { + write_letter (file, 'M', update_dir); + history_write ('G', update_dir, vers->vn_rcs, file, repository); + } + return (0); +} + +/* + * Do all the magic associated with a file which needs to be joined + * (-j option) + */ +static void +#ifdef SERVER_SUPPORT +join_file (file, rcsnode, vers, update_dir, entries, repository) + char *repository; +#else +join_file (file, rcsnode, vers, update_dir, entries) +#endif + char *file; + RCSNode *rcsnode; + Vers_TS *vers; + char *update_dir; + List *entries; +{ + char user[PATH_MAX]; + char backup[PATH_MAX]; + char *options; + int status; + + char *rev1; + char *rev2; + char *jrev1; + char *jrev2; + char *jdate1; + char *jdate2; + + jrev1 = join_rev1; + jrev2 = join_rev2; + jdate1 = date_rev1; + jdate2 = date_rev2; + + if (wrap_merge_is_copy (file)) + { + /* FIXME: Should be including update_dir in message. */ + error (0, 0, + "Cannot merge %s because it is a merge-by-copy file.", file); + return; + } + + /* determine if we need to do anything at all */ + if (vers->srcfile == NULL || + vers->srcfile->path == NULL) + { + return; + } + + /* in all cases, use two revs. */ + + /* if only one rev is specified, it becomes the second rev */ + if (jrev2 == NULL) + { + jrev2 = jrev1; + jrev1 = NULL; + jdate2 = jdate1; + jdate1 = NULL; + } + + /* The file in the working directory doesn't exist in CVS/Entries. + FIXME: Shouldn't this case result in additional processing (if + the file was added going from rev1 to rev2, then do the equivalent + of a "cvs add")? (yes; easier said than done.. :-) */ + if (vers->vn_user == NULL) + { + /* No merge possible YET. */ + if (jdate2 != NULL) + error (0, 0, + "file %s is present in revision %s as of %s", + file, jrev2, jdate2); + else + error (0, 0, + "file %s is present in revision %s", + file, jrev2); + return; + } + + /* Fix for bug CVS/193: + * Used to dump core if the file had been removed on the current branch. + */ + if (strcmp(vers->vn_user, "0") == 0) + { + error(0, 0, + "file %s has been deleted", + file); + return; + } + + /* convert the second rev spec, walking branches and dates. */ + + rev2 = RCS_getversion (vers->srcfile, jrev2, jdate2, 1, 0); + if (rev2 == NULL) + { + if (!quiet) + { + if (jdate2 != NULL) + error (0, 0, + "cannot find revision %s as of %s in file %s", + jrev2, jdate2, file); + else + error (0, 0, + "cannot find revision %s in file %s", + jrev2, file); + } + return; + } + + /* skip joining identical revs */ + if (strcmp (rev2, vers->vn_user) == 0) + { + /* No merge necessary. */ + free (rev2); + return; + } + + if (jrev1 == NULL) + { + char *tst; + /* if the first rev is missing, then it is implied to be the + greatest common ancestor of both the join rev, and the + checked out rev. */ + + /* FIXME: What is this check for '!' about? If it is legal to + have '!' in the first character of vn_user, it isn't + documented at struct vers_ts in cvs.h. */ + tst = vers->vn_user; + if (*tst == '!') + { + /* file was dead. merge anyway and pretend it's been + added. */ + ++tst; + Register (entries, file, "0", vers->ts_user, vers->options, + vers->tag, (char *) 0, (char *) 0); + } + rev1 = gca (tst, rev2); + if (rev1 == NULL) + { + /* this should not be possible */ + error (0, 0, "bad gca"); + abort(); + } + + tst = RCS_gettag (vers->srcfile, rev2, 1, 0); + if (tst == NULL) + { + /* this should not be possible. */ + error (0, 0, "cannot find gca"); + abort(); + } + + free (tst); + + /* these two cases are noops */ + if (strcmp (rev1, rev2) == 0) + { + free (rev1); + free (rev2); + return; + } + } + else + { + /* otherwise, convert the first rev spec, walking branches and + dates. */ + + rev1 = RCS_getversion (vers->srcfile, jrev1, jdate1, 1, 0); + if (rev1 == NULL) + { + if (!quiet) { + if (jdate1 != NULL) + error (0, 0, + "cannot find revision %s as of %s in file %s", + jrev1, jdate1, file); + else + error (0, 0, + "cannot find revision %s in file %s", + jrev1, file); + } + return; + } + } + + /* do the join */ + +#if 0 + dome { + /* special handling when two revisions are specified */ + if (join_rev1 && join_rev2) + { + rev = RCS_getversion (vers->srcfile, join_rev2, date_rev2, 1, 0); + if (rev == NULL) + { + if (!quiet && date_rev2 == NULL) + error (0, 0, + "cannot find revision %s in file %s", join_rev2, file); + return; + } + + baserev = RCS_getversion (vers->srcfile, join_rev1, date_rev1, 1, 0); + if (baserev == NULL) + { + if (!quiet && date_rev1 == NULL) + error (0, 0, + "cannot find revision %s in file %s", join_rev1, file); + free (rev); + return; + } + + /* + * nothing to do if: + * second revision matches our BASE revision (vn_user) && + * both revisions are on the same branch + */ + if (strcmp (vers->vn_user, rev) == 0 && + numdots (baserev) == numdots (rev)) + { + /* might be the same branch. take a real look */ + char *dot = strrchr (baserev, '.'); + int len = (dot - baserev) + 1; + + if (strncmp (baserev, rev, len) == 0) + return; + } + } + else + { + rev = RCS_getversion (vers->srcfile, join_rev1, date_rev1, 1, 0); + if (rev == NULL) + return; + if (strcmp (rev, vers->vn_user) == 0) /* no merge necessary */ + { + free (rev); + return; + } + + baserev = RCS_whatbranch (file, join_rev1, rcsnode); + if (baserev) + { + char *cp; + + /* we get a branch -- turn it into a revision, or NULL if trunk */ + if ((cp = strrchr (baserev, '.')) == NULL) + { + free (baserev); + baserev = (char *) NULL; + } + else + *cp = '\0'; + } + } + if (baserev && strcmp (baserev, rev) == 0) + { + /* they match -> nothing to do */ + free (rev); + free (baserev); + return; + } + } +#endif + + /* OK, so we have two revisions; continue on */ + +#ifdef SERVER_SUPPORT + if (server_active && !isreadable (file)) + { + int retcode; + /* The file is up to date. Need to check out the current contents. */ + retcode = RCS_checkout (vers->srcfile->path, "", vers->vn_user, NULL, + RUN_TTY, 0, 0); + if (retcode != 0) + error (1, retcode == -1 ? errno : 0, + "failed to check out %s file", file); + } +#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. + */ + (void) sprintf (backup, "%s%s.%s", BAKPREFIX, file, vers->vn_user); + if (update_dir[0]) + (void) sprintf (user, "%s/%s", update_dir, file); + else + (void) strcpy (user, file); + + (void) unlink_file (backup); + copy_file (file, backup); + xchmod (file, 1); + + options = vers->options; +#ifdef HAVE_RCS5 +#if 0 + if (*options == '\0') + options = "-kk"; /* to ignore keyword expansions */ +#endif +#endif + + status = RCS_merge (vers->srcfile->path, options, rev1, rev2); + if (status != 0 && status != 1) + { + error (0, status == -1 ? errno : 0, + "could not merge revision %s of %s", rev2, user); + error (status == -1 ? 1 : 0, 0, "restoring %s from backup file %s", + user, backup); + rename_file (backup, file); + } + free (rev1); + free (rev2); + +#ifdef SERVER_SUPPORT + /* + * If we're in server mode, then we need to re-register the file + * even if there were no conflicts (status == 0). + * This tells server_updated() to send the modified file back to + * the client. + */ + if (status == 1 || (status == 0 && server_active)) +#else + if (status == 1) +#endif + { + char *cp = 0; + + if (status) + cp = time_stamp (file); + Register (entries, file, vers->vn_rcs, vers->ts_rcs, vers->options, + vers->tag, vers->date, cp); + if (cp) + free(cp); + } + +#ifdef SERVER_SUPPORT + if (server_active) + { + server_copy_file (file, update_dir, repository, backup); + server_updated (file, update_dir, repository, SERVER_MERGED, + (struct stat *) NULL, (unsigned char *) NULL); + } +#endif +} + +int +joining () +{ + return (join_rev1 != NULL); +} |