diff options
Diffstat (limited to 'contrib/cvs/src/commit.c')
-rw-r--r-- | contrib/cvs/src/commit.c | 2433 |
1 files changed, 2433 insertions, 0 deletions
diff --git a/contrib/cvs/src/commit.c b/contrib/cvs/src/commit.c new file mode 100644 index 0000000..b3ba47b --- /dev/null +++ b/contrib/cvs/src/commit.c @@ -0,0 +1,2433 @@ +/* + * 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. + * + * Commit Files + * + * "commit" commits the present version to the RCS repository, AFTER + * having done a test on conflicts. + * + * The call is: cvs commit [options] files... + * + * $FreeBSD$ + */ + +#include <assert.h> +#include "cvs.h" +#include "getline.h" +#include "edit.h" +#include "fileattr.h" +#include "hardlink.h" + +static Dtype check_direntproc PROTO ((void *callerdat, const char *dir, + const char *repos, + const char *update_dir, + List *entries)); +static int check_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int check_filesdoneproc PROTO ((void *callerdat, int err, + const char *repos, + const char *update_dir, + List *entries)); +static int checkaddfile PROTO((const char *file, const char *repository, + const char *tag, const char *options, + RCSNode **rcsnode)); +static Dtype commit_direntproc PROTO ((void *callerdat, const char *dir, + const char *repos, + const char *update_dir, + List *entries)); +static int commit_dirleaveproc PROTO ((void *callerdat, const char *dir, + int err, const char *update_dir, + List *entries)); +static int commit_fileproc PROTO ((void *callerdat, struct file_info *finfo)); +static int commit_filesdoneproc PROTO ((void *callerdat, int err, + const char *repository, + const char *update_dir, + List *entries)); +static int finaladd PROTO((struct file_info *finfo, char *revision, char *tag, + char *options)); +static int findmaxrev PROTO((Node * p, void *closure)); +static int lock_RCS PROTO((const char *user, RCSNode *rcs, const char *rev, + const char *repository)); +static int precommit_list_proc PROTO((Node * p, void *closure)); +static int precommit_proc PROTO((const char *repository, const char *filter)); +static int remove_file PROTO ((struct file_info *finfo, char *tag, + char *message)); +static void fixaddfile PROTO((const char *rcs)); +static void fixbranch PROTO((RCSNode *, char *branch)); +static void unlockrcs PROTO((RCSNode *rcs)); +static void ci_delproc PROTO((Node *p)); +static void masterlist_delproc PROTO((Node *p)); + +struct commit_info +{ + Ctype status; /* as returned from Classify_File() */ + char *rev; /* a numeric rev, if we know it */ + char *tag; /* any sticky tag, or -r option */ + char *options; /* Any sticky -k option */ +}; +struct master_lists +{ + List *ulist; /* list for Update_Logfile */ + List *cilist; /* list with commit_info structs */ +}; + +static int force_ci = 0; +static int got_message; +static int aflag; +static char *saved_tag; +static char *write_dirtag; +static int write_dirnonbranch; +static char *logfile; +static List *mulist; +static List *saved_ulist; +static char *saved_message; +static time_t last_register_time; + +static const char *const commit_usage[] = +{ + "Usage: %s %s [-Rlf] [-m msg | -F logfile] [-r rev] files...\n", + " -R Process directories recursively.\n", + " -l Local directory only (not recursive).\n", + " -f Force the file to be committed; disables recursion.\n", + " -F logfile Read the log message from file.\n", + " -m msg Log message.\n", + " -r rev Commit to this branch or trunk revision.\n", + "(Specify the --help global option for a list of other help options)\n", + NULL +}; + +#ifdef CLIENT_SUPPORT +/* Identify a file which needs "? foo" or a Questionable request. */ +struct question { + /* The two fields for the Directory request. */ + char *dir; + char *repos; + + /* The file name. */ + char *file; + + struct question *next; +}; + +struct find_data { + List *ulist; + int argc; + char **argv; + + /* This is used from dirent to filesdone time, for each directory, + to make a list of files we have already seen. */ + List *ignlist; + + /* Linked list of files which need "? foo" or a Questionable request. */ + struct question *questionables; + + /* Only good within functions called from the filesdoneproc. Stores + the repository (pointer into storage managed by the recursion + processor. */ + const char *repository; + + /* Non-zero if we should force the commit. This is enabled by + either -f or -r options, unlike force_ci which is just -f. */ + int force; +}; + + + +static Dtype find_dirent_proc PROTO ((void *callerdat, const char *dir, + const char *repository, + const char *update_dir, + List *entries)); + +static Dtype +find_dirent_proc (callerdat, dir, repository, update_dir, entries) + void *callerdat; + const char *dir; + const char *repository; + const char *update_dir; + List *entries; +{ + struct find_data *find_data = (struct find_data *)callerdat; + + /* This check seems to slowly be creeping throughout CVS (update + and send_dirent_proc by CVS 1.5, diff in 31 Oct 1995. My guess + is that it (or some variant thereof) should go in all the + dirent procs. Unless someone has some better idea... */ + if (!isdir (dir)) + return R_SKIP_ALL; + + /* initialize the ignore list for this directory */ + find_data->ignlist = getlist (); + + /* Print the same warm fuzzy as in check_direntproc, since that + code will never be run during client/server operation and we + want the messages to match. */ + if (!quiet) + error (0, 0, "Examining %s", update_dir); + + return R_PROCESS; +} + + + +/* Here as a static until we get around to fixing ignore_files to pass + it along as an argument. */ +static struct find_data *find_data_static; + + + +static void find_ignproc PROTO ((const char *, const char *)); + +static void +find_ignproc (file, dir) + const char *file; + const char *dir; +{ + struct question *p; + + p = (struct question *) xmalloc (sizeof (struct question)); + p->dir = xstrdup (dir); + p->repos = xstrdup (find_data_static->repository); + p->file = xstrdup (file); + p->next = find_data_static->questionables; + find_data_static->questionables = p; +} + + + +static int find_filesdoneproc PROTO ((void *callerdat, int err, + const char *repository, + const char *update_dir, + List *entries)); + +static int +find_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + const char *repository; + const char *update_dir; + List *entries; +{ + struct find_data *find_data = (struct find_data *)callerdat; + find_data->repository = repository; + + /* if this directory has an ignore list, process it then free it */ + if (find_data->ignlist) + { + find_data_static = find_data; + ignore_files (find_data->ignlist, entries, update_dir, find_ignproc); + dellist (&find_data->ignlist); + } + + find_data->repository = NULL; + + return err; +} + + + +static int find_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +/* Machinery to find out what is modified, added, and removed. It is + possible this should be broken out into a new client_classify function; + merging it with classify_file is almost sure to be a mess, though, + because classify_file has all kinds of repository processing. */ +static int +find_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + Vers_TS *vers; + enum classify_type status; + Node *node; + struct find_data *args = (struct find_data *)callerdat; + struct logfile_info *data; + struct file_info xfinfo; + + /* if this directory has an ignore list, add this file to it */ + if (args->ignlist) + { + Node *p; + + p = getnode (); + p->type = FILES; + p->key = xstrdup (finfo->file); + if (addnode (args->ignlist, p) != 0) + freenode (p); + } + + xfinfo = *finfo; + xfinfo.repository = NULL; + xfinfo.rcs = NULL; + + vers = Version_TS (&xfinfo, NULL, saved_tag, NULL, 0, 0); + if (vers->vn_user == NULL) + { + if (vers->ts_user == NULL) + error (0, 0, "nothing known about `%s'", finfo->fullname); + else + error (0, 0, "use `%s add' to create an entry for %s", + program_name, finfo->fullname); + freevers_ts (&vers); + return 1; + } + if (vers->vn_user[0] == '-') + { + if (vers->ts_user != NULL) + { + error (0, 0, + "`%s' should be removed and is still there (or is back" + " again)", finfo->fullname); + freevers_ts (&vers); + return 1; + } + /* else */ + status = T_REMOVED; + } + else if (strcmp (vers->vn_user, "0") == 0) + { + if (vers->ts_user == NULL) + { + /* This happens when one has `cvs add'ed a file, but it no + longer exists in the working directory at commit time. + FIXME: What classify_file does in this case is print + "new-born %s has disappeared" and removes the entry. + We probably should do the same. */ + if (!really_quiet) + error (0, 0, "warning: new-born %s has disappeared", + finfo->fullname); + status = T_REMOVE_ENTRY; + } + else + status = T_ADDED; + } + else if (vers->ts_user == NULL) + { + /* FIXME: What classify_file does in this case is print + "%s was lost". We probably should do the same. */ + freevers_ts (&vers); + return 0; + } + else if (vers->ts_rcs != NULL + && (args->force || strcmp (vers->ts_user, vers->ts_rcs) != 0)) + /* If we are forcing commits, pretend that the file is + modified. */ + status = T_MODIFIED; + else + { + /* This covers unmodified files, as well as a variety of other + cases. FIXME: we probably should be printing a message and + returning 1 for many of those cases (but I'm not sure + exactly which ones). */ + freevers_ts (&vers); + return 0; + } + + node = getnode (); + node->key = xstrdup (finfo->fullname); + + data = (struct logfile_info *) xmalloc (sizeof (struct logfile_info)); + data->type = status; + data->tag = xstrdup (vers->tag); + data->rev_old = data->rev_new = NULL; + + node->type = UPDATE; + node->delproc = update_delproc; + node->data = data; + (void)addnode (args->ulist, node); + + ++args->argc; + + freevers_ts (&vers); + return 0; +} + + + +static int copy_ulist PROTO ((Node *, void *)); + +static int +copy_ulist (node, data) + Node *node; + void *data; +{ + struct find_data *args = (struct find_data *)data; + args->argv[args->argc++] = node->key; + return 0; +} +#endif /* CLIENT_SUPPORT */ + +#ifdef SERVER_SUPPORT +# define COMMIT_OPTIONS "+nlRm:fF:r:" +#else /* !SERVER_SUPPORT */ +# define COMMIT_OPTIONS "+lRm:fF:r:" +#endif /* SERVER_SUPPORT */ +int +commit (argc, argv) + int argc; + char **argv; +{ + int c; + int err = 0; + int local = 0; + + if (argc == -1) + usage (commit_usage); + +#ifdef CVS_BADROOT + /* + * For log purposes, do not allow "root" to commit files. If you look + * like root, but are really logged in as a non-root user, it's OK. + */ + /* FIXME: Shouldn't this check be much more closely related to the + readonly user stuff (CVSROOT/readers, &c). That is, why should + root be able to "cvs init", "cvs import", &c, but not "cvs ci"? */ + /* Who we are on the client side doesn't affect logging. */ + if (geteuid () == (uid_t) 0 && !current_parsed_root->isremote) + { + struct passwd *pw; + + if ((pw = (struct passwd *) getpwnam (getcaller ())) == NULL) + error (1, 0, + "your apparent username (%s) is unknown to this system", + getcaller ()); + if (pw->pw_uid == (uid_t) 0) + error (1, 0, "'root' is not allowed to commit files"); + } +#endif /* CVS_BADROOT */ + + optind = 0; + while ((c = getopt (argc, argv, COMMIT_OPTIONS)) != -1) + { + switch (c) + { +#ifdef SERVER_SUPPORT + case 'n': + /* Silently ignore -n for compatibility with old + * clients. + */ + if (!server_active) error(0, 0, "the `-n' option is obsolete"); + break; +#endif /* SERVER_SUPPORT */ + case 'm': +#ifdef FORCE_USE_EDITOR + use_editor = 1; +#else + use_editor = 0; +#endif + if (saved_message) + { + free (saved_message); + saved_message = NULL; + } + + saved_message = xstrdup(optarg); + break; + case 'r': + if (saved_tag) + free (saved_tag); + saved_tag = xstrdup (optarg); + break; + case 'l': + local = 1; + break; + case 'R': + local = 0; + break; + case 'f': + force_ci = 1; + local = 1; /* also disable recursion */ + break; + case 'F': +#ifdef FORCE_USE_EDITOR + use_editor = 1; +#else + use_editor = 0; +#endif + logfile = optarg; + break; + case '?': + default: + usage (commit_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* numeric specified revision means we ignore sticky tags... */ + if (saved_tag && isdigit ((unsigned char) *saved_tag)) + { + char *p = saved_tag + strlen (saved_tag); + aflag = 1; + /* strip trailing dots and leading zeros */ + while (*--p == '.') ; + p[1] = '\0'; + while (saved_tag[0] == '0' && isdigit ((unsigned char) saved_tag[1])) + ++saved_tag; + } + + /* some checks related to the "-F logfile" option */ + if (logfile) + { + size_t size = 0, len; + + if (saved_message) + error (1, 0, "cannot specify both a message and a log file"); + + get_file (logfile, logfile, "r", &saved_message, &size, &len); + } + +#ifdef CLIENT_SUPPORT + if (current_parsed_root->isremote) + { + struct find_data find_args; + + ign_setup (); + + find_args.ulist = getlist (); + find_args.argc = 0; + find_args.questionables = NULL; + find_args.ignlist = NULL; + find_args.repository = NULL; + + /* It is possible that only a numeric tag should set this. + I haven't really thought about it much. + Anyway, I suspect that setting it unnecessarily only causes + a little unneeded network traffic. */ + find_args.force = force_ci || saved_tag != NULL; + + err = start_recursion (find_fileproc, find_filesdoneproc, + find_dirent_proc, (DIRLEAVEPROC) NULL, + (void *)&find_args, + argc, argv, local, W_LOCAL, 0, CVS_LOCK_NONE, + (char *) NULL, 0, (char *) NULL); + if (err) + error (1, 0, "correct above errors first!"); + + if (find_args.argc == 0) + { + /* Nothing to commit. Exit now without contacting the + server (note that this means that we won't print "? + foo" for files which merit it, because we don't know + what is in the CVSROOT/cvsignore file). */ + dellist (&find_args.ulist); + return 0; + } + + /* Now we keep track of which files we actually are going to + operate on, and only work with those files in the future. + This saves time--we don't want to search the file system + of the working directory twice. */ + if (size_overflow_p (xtimes (find_args.argc, sizeof (char **)))) + { + find_args.argc = 0; + return 0; + } + find_args.argv = xmalloc (xtimes (find_args.argc, sizeof (char **))); + find_args.argc = 0; + walklist (find_args.ulist, copy_ulist, &find_args); + + /* Do this before calling do_editor; don't ask for a log + message if we can't talk to the server. But do it after we + have made the checks that we can locally (to more quickly + catch syntax errors, the case where no files are modified, + added or removed, etc.). + + On the other hand, calling start_server before do_editor + means that we chew up server resources the whole time that + the user has the editor open (hours or days if the user + forgets about it), which seems dubious. */ + start_server (); + + /* + * We do this once, not once for each directory as in normal CVS. + * The protocol is designed this way. This is a feature. + */ + if (use_editor) + do_editor (".", &saved_message, (char *)NULL, find_args.ulist); + + /* We always send some sort of message, even if empty. */ + option_with_arg ("-m", saved_message ? saved_message : ""); + + /* OK, now process all the questionable files we have been saving + up. */ + { + struct question *p; + struct question *q; + + p = find_args.questionables; + while (p != NULL) + { + if (ign_inhibit_server || !supported_request ("Questionable")) + { + cvs_output ("? ", 2); + if (p->dir[0] != '\0') + { + cvs_output (p->dir, 0); + cvs_output ("/", 1); + } + cvs_output (p->file, 0); + cvs_output ("\n", 1); + } + else + { + send_to_server ("Directory ", 0); + send_to_server (p->dir[0] == '\0' ? "." : p->dir, 0); + send_to_server ("\012", 1); + send_to_server (p->repos, 0); + send_to_server ("\012", 1); + + send_to_server ("Questionable ", 0); + send_to_server (p->file, 0); + send_to_server ("\012", 1); + } + free (p->dir); + free (p->repos); + free (p->file); + q = p->next; + free (p); + p = q; + } + } + + if (local) + send_arg("-l"); + if (force_ci) + send_arg("-f"); + option_with_arg ("-r", saved_tag); + send_arg ("--"); + + /* FIXME: This whole find_args.force/SEND_FORCE business is a + kludge. It would seem to be a server bug that we have to + say that files are modified when they are not. This makes + "cvs commit -r 2" across a whole bunch of files a very slow + operation (and it isn't documented in cvsclient.texi). I + haven't looked at the server code carefully enough to be + _sure_ why this is needed, but if it is because the "ci" + program, which we used to call, wanted the file to exist, + then it would be relatively simple to fix in the server. */ + send_files (find_args.argc, find_args.argv, local, 0, + find_args.force ? SEND_FORCE : 0); + + /* Sending only the names of the files which were modified, added, + or removed means that the server will only do an up-to-date + check on those files. This is different from local CVS and + previous versions of client/server CVS, but it probably is a Good + Thing, or at least Not Such A Bad Thing. */ + send_file_names (find_args.argc, find_args.argv, 0); + free (find_args.argv); + dellist (&find_args.ulist); + + send_to_server ("ci\012", 0); + err = get_responses_and_close (); + if (err != 0 && use_editor && saved_message != NULL) + { + /* If there was an error, don't nuke the user's carefully + constructed prose. This is something of a kludge; a better + solution is probably more along the lines of #150 in TODO + (doing a second up-to-date check before accepting the + log message has also been suggested, but that seems kind of + iffy because the real up-to-date check could still fail, + another error could occur, &c. Also, a second check would + slow things down). */ + + char *fname; + FILE *fp; + + fp = cvs_temp_file (&fname); + if (fp == NULL) + error (1, 0, "cannot create temporary file %s", + fname ? fname : "(null)"); + if (fwrite (saved_message, 1, strlen (saved_message), fp) + != strlen (saved_message)) + error (1, errno, "cannot write temporary file %s", fname); + if (fclose (fp) < 0) + error (0, errno, "cannot close temporary file %s", fname); + error (0, 0, "saving log message in %s", fname); + free (fname); + } + return err; + } +#endif + + if (saved_tag != NULL) + tag_check_valid (saved_tag, argc, argv, local, aflag, ""); + + /* XXX - this is not the perfect check for this */ + if (argc <= 0) + write_dirtag = saved_tag; + + wrap_setup (); + + lock_tree_for_write (argc, argv, local, W_LOCAL, aflag); + + /* + * Set up the master update list and hard link list + */ + mulist = getlist (); + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (preserve_perms) + { + hardlist = getlist (); + + /* + * We need to save the working directory so that + * check_fileproc can construct a full pathname for each file. + */ + working_dir = xgetwd(); + } +#endif + + /* + * Run the recursion processor to verify the files are all up-to-date + */ + err = start_recursion (check_fileproc, check_filesdoneproc, + check_direntproc, (DIRLEAVEPROC) NULL, NULL, argc, + argv, local, W_LOCAL, aflag, CVS_LOCK_NONE, + (char *) NULL, 1, (char *) NULL); + if (err) + { + Lock_Cleanup (); + error (1, 0, "correct above errors first!"); + } + + /* + * Run the recursion processor to commit the files + */ + write_dirnonbranch = 0; + if (noexec == 0) + err = start_recursion (commit_fileproc, commit_filesdoneproc, + commit_direntproc, commit_dirleaveproc, NULL, + argc, argv, local, W_LOCAL, aflag, CVS_LOCK_NONE, + (char *) NULL, 1, (char *) NULL); + + /* + * Unlock all the dirs and clean up + */ + Lock_Cleanup (); + dellist (&mulist); + + if (server_active) + return err; + + /* see if we need to sleep before returning to avoid time-stamp races */ + if (last_register_time) + { + sleep_past (last_register_time); + } + + return err; +} + + + +/* This routine determines the status of a given file and retrieves + the version information that is associated with that file. */ + +static +Ctype +classify_file_internal (finfo, vers) + struct file_info *finfo; + Vers_TS **vers; +{ + int save_noexec, save_quiet, save_really_quiet; + Ctype status; + + /* FIXME: Do we need to save quiet as well as really_quiet? Last + time I glanced at Classify_File I only saw it looking at really_quiet + not quiet. */ + save_noexec = noexec; + save_quiet = quiet; + save_really_quiet = really_quiet; + noexec = quiet = really_quiet = 1; + + /* handle specified numeric revision specially */ + if (saved_tag && isdigit ((unsigned char) *saved_tag)) + { + /* If the tag is for the trunk, make sure we're at the head */ + if (numdots (saved_tag) < 2) + { + status = Classify_File (finfo, (char *) NULL, (char *) NULL, + (char *) NULL, 1, aflag, vers, 0); + if (status == T_UPTODATE || status == T_MODIFIED || + status == T_ADDED) + { + Ctype xstatus; + + freevers_ts (vers); + xstatus = Classify_File (finfo, saved_tag, (char *) NULL, + (char *) NULL, 1, aflag, vers, 0); + if (xstatus == T_REMOVE_ENTRY) + status = T_MODIFIED; + else if (status == T_MODIFIED && xstatus == T_CONFLICT) + status = T_MODIFIED; + else + status = xstatus; + } + } + else + { + char *xtag, *cp; + + /* + * The revision is off the main trunk; make sure we're + * up-to-date with the head of the specified branch. + */ + xtag = xstrdup (saved_tag); + if ((numdots (xtag) & 1) != 0) + { + cp = strrchr (xtag, '.'); + *cp = '\0'; + } + status = Classify_File (finfo, xtag, (char *) NULL, + (char *) NULL, 1, aflag, vers, 0); + if ((status == T_REMOVE_ENTRY || status == T_CONFLICT) + && (cp = strrchr (xtag, '.')) != NULL) + { + /* pluck one more dot off the revision */ + *cp = '\0'; + freevers_ts (vers); + status = Classify_File (finfo, xtag, (char *) NULL, + (char *) NULL, 1, aflag, vers, 0); + if (status == T_UPTODATE || status == T_REMOVE_ENTRY) + status = T_MODIFIED; + } + /* now, muck with vers to make the tag correct */ + free ((*vers)->tag); + (*vers)->tag = xstrdup (saved_tag); + free (xtag); + } + } + else + status = Classify_File (finfo, saved_tag, (char *) NULL, (char *) NULL, + 1, 0, vers, 0); + noexec = save_noexec; + quiet = save_quiet; + really_quiet = save_really_quiet; + + return status; +} + + + +/* + * Check to see if a file is ok to commit and make sure all files are + * up-to-date + */ +/* ARGSUSED */ +static int +check_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + Ctype status; + const char *xdir; + Node *p; + List *ulist, *cilist; + Vers_TS *vers; + struct commit_info *ci; + struct logfile_info *li; + + size_t cvsroot_len = strlen (current_parsed_root->directory); + + if (!finfo->repository) + { + error (0, 0, "nothing known about `%s'", finfo->fullname); + return 1; + } + + if (strncmp (finfo->repository, current_parsed_root->directory, + cvsroot_len) == 0 + && ISDIRSEP (finfo->repository[cvsroot_len]) + && strncmp (finfo->repository + cvsroot_len + 1, + CVSROOTADM, + sizeof (CVSROOTADM) - 1) == 0 + && ISDIRSEP (finfo->repository[cvsroot_len + sizeof (CVSROOTADM)]) + && strcmp (finfo->repository + cvsroot_len + sizeof (CVSROOTADM) + 1, + CVSNULLREPOS) == 0 + ) + error (1, 0, "cannot check in to %s", finfo->repository); + + status = classify_file_internal (finfo, &vers); + + /* + * If the force-commit option is enabled, and the file in question + * appears to be up-to-date, just make it look modified so that + * it will be committed. + */ + if (force_ci && status == T_UPTODATE) + status = T_MODIFIED; + + switch (status) + { + case T_CHECKOUT: + case T_PATCH: + case T_NEEDS_MERGE: + case T_REMOVE_ENTRY: + error (0, 0, "Up-to-date check failed for `%s'", finfo->fullname); + freevers_ts (&vers); + return 1; + case T_CONFLICT: + case T_MODIFIED: + case T_ADDED: + case T_REMOVED: + /* + * some quick sanity checks; if no numeric -r option specified: + * - can't have a sticky date + * - can't have a sticky tag that is not a branch + * Also, + * - if status is T_REMOVED, file must not exist and its entry + * can't have a numeric sticky tag. + * - if status is T_ADDED, rcs file must not exist unless on + * a branch or head is dead + * - if status is T_ADDED, can't have a non-trunk numeric rev + * - if status is T_MODIFIED and a Conflict marker exists, don't + * allow the commit if timestamp is identical or if we find + * an RCS_MERGE_PAT in the file. + */ + if (!saved_tag || !isdigit ((unsigned char) *saved_tag)) + { + if (vers->date) + { + error (0, 0, + "cannot commit with sticky date for file `%s'", + finfo->fullname); + freevers_ts (&vers); + return 1; + } + if (status == T_MODIFIED && vers->tag && + !RCS_isbranch (finfo->rcs, vers->tag)) + { + error (0, 0, + "sticky tag `%s' for file `%s' is not a branch", + vers->tag, finfo->fullname); + freevers_ts (&vers); + return 1; + } + } + if (status == T_CONFLICT && !force_ci) + { + error (0, 0, + "file `%s' had a conflict and has not been modified", + finfo->fullname); + freevers_ts (&vers); + return 1; + } + if (status == T_MODIFIED && !force_ci && file_has_markers (finfo)) + { + /* Make this a warning, not an error, because we have + no way of knowing whether the "conflict indicators" + are really from a conflict or whether they are part + of the document itself (cvs.texinfo and sanity.sh in + CVS itself, for example, tend to want to have strings + like ">>>>>>>" at the start of a line). Making people + kludge this the way they need to kludge keyword + expansion seems undesirable. And it is worse than + keyword expansion, because there is no -ko + analogue. */ + error (0, 0, + "\ +warning: file `%s' seems to still contain conflict indicators", + finfo->fullname); + } + + if (status == T_REMOVED) + { + if (vers->ts_user != NULL) + { + error (0, 0, + "`%s' should be removed and is still there (or is" + " back again)", finfo->fullname); + freevers_ts (&vers); + return 1; + } + + if (vers->tag && isdigit ((unsigned char) *vers->tag)) + { + /* Remove also tries to forbid this, but we should check + here. I'm only _sure_ about somewhat obscure cases + (hacking the Entries file, using an old version of + CVS for the remove and a new one for the commit), but + there might be other cases. */ + error (0, 0, + "cannot remove file `%s' which has a numeric sticky" + " tag of `%s'", finfo->fullname, vers->tag); + freevers_ts (&vers); + return 1; + } + } + if (status == T_ADDED) + { + if (vers->tag == NULL) + { + if (finfo->rcs != NULL && + !RCS_isdead (finfo->rcs, finfo->rcs->head)) + { + error (0, 0, + "cannot add file `%s' when RCS file `%s' already exists", + finfo->fullname, finfo->rcs->path); + freevers_ts (&vers); + return 1; + } + } + else if (isdigit ((unsigned char) *vers->tag) && + numdots (vers->tag) > 1) + { + error (0, 0, + "cannot add file `%s' with revision `%s'; must be on trunk", + finfo->fullname, vers->tag); + freevers_ts (&vers); + return 1; + } + } + + /* done with consistency checks; now, to get on with the commit */ + if (finfo->update_dir[0] == '\0') + xdir = "."; + else + xdir = finfo->update_dir; + if ((p = findnode (mulist, xdir)) != NULL) + { + ulist = ((struct master_lists *) p->data)->ulist; + cilist = ((struct master_lists *) p->data)->cilist; + } + else + { + struct master_lists *ml; + + ulist = getlist (); + cilist = getlist (); + p = getnode (); + p->key = xstrdup (xdir); + p->type = UPDATE; + ml = (struct master_lists *) + xmalloc (sizeof (struct master_lists)); + ml->ulist = ulist; + ml->cilist = cilist; + p->data = ml; + p->delproc = masterlist_delproc; + (void) addnode (mulist, p); + } + + /* first do ulist, then cilist */ + p = getnode (); + p->key = xstrdup (finfo->file); + p->type = UPDATE; + p->delproc = update_delproc; + li = ((struct logfile_info *) + xmalloc (sizeof (struct logfile_info))); + li->type = status; + li->tag = xstrdup (vers->tag); + li->rev_old = xstrdup (vers->vn_rcs); + li->rev_new = NULL; + p->data = li; + (void) addnode (ulist, p); + + p = getnode (); + p->key = xstrdup (finfo->file); + p->type = UPDATE; + p->delproc = ci_delproc; + ci = (struct commit_info *) xmalloc (sizeof (struct commit_info)); + ci->status = status; + if (vers->tag) + if (isdigit ((unsigned char) *vers->tag)) + ci->rev = xstrdup (vers->tag); + else + ci->rev = RCS_whatbranch (finfo->rcs, vers->tag); + else + ci->rev = (char *) NULL; + ci->tag = xstrdup (vers->tag); + ci->options = xstrdup(vers->options); + p->data = ci; + (void) addnode (cilist, p); + +#ifdef PRESERVE_PERMISSIONS_SUPPORT + if (preserve_perms) + { + /* Add this file to hardlist, indexed on its inode. When + we are done, we can find out what files are hardlinked + to a given file by looking up its inode in hardlist. */ + char *fullpath; + Node *linkp; + struct hardlink_info *hlinfo; + + /* Get the full pathname of the current file. */ + fullpath = xmalloc (strlen(working_dir) + + strlen(finfo->fullname) + 2); + sprintf (fullpath, "%s/%s", working_dir, finfo->fullname); + + /* To permit following links in subdirectories, files + are keyed on finfo->fullname, not on finfo->name. */ + linkp = lookup_file_by_inode (fullpath); + + /* If linkp is NULL, the file doesn't exist... maybe + we're doing a remove operation? */ + if (linkp != NULL) + { + /* Create a new hardlink_info node, which will record + the current file's status and the links listed in its + `hardlinks' delta field. We will append this + hardlink_info node to the appropriate hardlist entry. */ + hlinfo = (struct hardlink_info *) + xmalloc (sizeof (struct hardlink_info)); + hlinfo->status = status; + linkp->data = hlinfo; + } + } +#endif + + break; + case T_UNKNOWN: + error (0, 0, "nothing known about `%s'", finfo->fullname); + freevers_ts (&vers); + return 1; + case T_UPTODATE: + break; + default: + error (0, 0, "CVS internal error: unknown status %d", status); + break; + } + + freevers_ts (&vers); + return 0; +} + + + +/* + * By default, return the code that tells do_recursion to examine all + * directories + */ +/* ARGSUSED */ +static Dtype +check_direntproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + const char *dir; + const char *repos; + const char *update_dir; + List *entries; +{ + if (!isdir (dir)) + return R_SKIP_ALL; + + if (!quiet) + error (0, 0, "Examining %s", update_dir); + + return R_PROCESS; +} + + + +/* + * Walklist proc to run pre-commit checks + */ +static int +precommit_list_proc (p, closure) + Node *p; + void *closure; +{ + struct logfile_info *li = p->data; + if (li->type == T_ADDED + || li->type == T_MODIFIED + || li->type == T_REMOVED) + { + run_arg (p->key); + } + return 0; +} + + + +/* + * Callback proc for pre-commit checking + */ +static int +precommit_proc (repository, filter) + const char *repository; + const char *filter; +{ + /* see if the filter is there, only if it's a full path */ + if (isabsolute (filter)) + { + char *s, *cp; + + s = xstrdup (filter); + for (cp = s; *cp; cp++) + if (isspace ((unsigned char) *cp)) + { + *cp = '\0'; + break; + } + if (!isfile (s)) + { + error (0, errno, "cannot find pre-commit filter `%s'", s); + free (s); + return 1; /* so it fails! */ + } + free (s); + } + + run_setup (filter); + run_arg (repository); + (void) walklist (saved_ulist, precommit_list_proc, NULL); + return run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL|RUN_REALLY); +} + + + +/* + * Run the pre-commit checks for the dir + */ +/* ARGSUSED */ +static int +check_filesdoneproc (callerdat, err, repos, update_dir, entries) + void *callerdat; + int err; + const char *repos; + const char *update_dir; + List *entries; +{ + int n; + Node *p; + + /* find the update list for this dir */ + p = findnode (mulist, update_dir); + if (p != NULL) + saved_ulist = ((struct master_lists *) p->data)->ulist; + else + saved_ulist = (List *) NULL; + + /* skip the checks if there's nothing to do */ + if (saved_ulist == NULL || saved_ulist->list->next == saved_ulist->list) + return err; + + /* run any pre-commit checks */ + if ((n = Parse_Info (CVSROOTADM_COMMITINFO, repos, precommit_proc, 1)) > 0) + { + error (0, 0, "Pre-commit check failed"); + err += n; + } + + return err; +} + + + +/* + * Do the work of committing a file + */ +static int maxrev; +static char *sbranch; + +/* ARGSUSED */ +static int +commit_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + Node *p; + int err = 0; + List *ulist, *cilist; + struct commit_info *ci; + + /* Keep track of whether write_dirtag 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. It is possible that case + should elicit a warning or an error. */ + if (write_dirtag != NULL + && finfo->rcs != NULL) + { + char *rev = RCS_getversion (finfo->rcs, write_dirtag, NULL, 1, NULL); + if (rev != NULL + && !RCS_nodeisbranch (finfo->rcs, write_dirtag)) + write_dirnonbranch = 1; + if (rev != NULL) + free (rev); + } + + if (finfo->update_dir[0] == '\0') + p = findnode (mulist, "."); + else + p = findnode (mulist, finfo->update_dir); + + /* + * if p is null, there were file type command line args which were + * all up-to-date so nothing really needs to be done + */ + if (p == NULL) + return 0; + ulist = ((struct master_lists *) p->data)->ulist; + cilist = ((struct master_lists *) p->data)->cilist; + + /* + * At this point, we should have the commit message unless we were called + * with files as args from the command line. In that latter case, we + * need to get the commit message ourselves + */ + if (!got_message) + { + got_message = 1; + if (!server_active && use_editor) + do_editor (finfo->update_dir, &saved_message, + finfo->repository, ulist); + do_verify (&saved_message, finfo->repository); + } + + p = findnode (cilist, finfo->file); + if (p == NULL) + return 0; + + ci = p->data; + if (ci->status == T_MODIFIED) + { + if (finfo->rcs == NULL) + error (1, 0, "internal error: no parsed RCS file"); + if (lock_RCS (finfo->file, finfo->rcs, ci->rev, + finfo->repository) != 0) + { + unlockrcs (finfo->rcs); + err = 1; + goto out; + } + } + else if (ci->status == T_ADDED) + { + if (checkaddfile (finfo->file, finfo->repository, ci->tag, ci->options, + &finfo->rcs) != 0) + { + if (finfo->rcs != NULL) + fixaddfile (finfo->rcs->path); + err = 1; + goto out; + } + + /* adding files with a tag, now means adding them on a branch. + Since the branch test was done in check_fileproc for + modified files, we need to stub it in again here. */ + + if (ci->tag + + /* If numeric, it is on the trunk; check_fileproc enforced + this. */ + && !isdigit ((unsigned char) ci->tag[0])) + { + if (finfo->rcs == NULL) + error (1, 0, "internal error: no parsed RCS file"); + if (ci->rev) + free (ci->rev); + ci->rev = RCS_whatbranch (finfo->rcs, ci->tag); + err = Checkin ('A', finfo, ci->rev, + ci->tag, ci->options, saved_message); + if (err != 0) + { + unlockrcs (finfo->rcs); + fixbranch (finfo->rcs, sbranch); + } + + (void) time (&last_register_time); + + ci->status = T_UPTODATE; + } + } + + /* + * Add the file for real + */ + if (ci->status == T_ADDED) + { + char *xrev = (char *) NULL; + + if (ci->rev == NULL) + { + /* find the max major rev number in this directory */ + maxrev = 0; + (void) walklist (finfo->entries, findmaxrev, NULL); + if (finfo->rcs->head) { + /* resurrecting: include dead revision */ + int thisrev = atoi (finfo->rcs->head); + if (thisrev > maxrev) + maxrev = thisrev; + } + if (maxrev == 0) + maxrev = 1; + xrev = xmalloc (20); + (void) sprintf (xrev, "%d", maxrev); + } + + /* XXX - an added file with symbolic -r should add tag as well */ + err = finaladd (finfo, ci->rev ? ci->rev : xrev, ci->tag, ci->options); + if (xrev) + free (xrev); + } + else if (ci->status == T_MODIFIED) + { + err = Checkin ('M', finfo, ci->rev, ci->tag, + ci->options, saved_message); + + (void) time (&last_register_time); + + if (err != 0) + { + unlockrcs (finfo->rcs); + fixbranch (finfo->rcs, sbranch); + } + } + else if (ci->status == T_REMOVED) + { + err = remove_file (finfo, ci->tag, saved_message); +#ifdef SERVER_SUPPORT + if (server_active) { + server_scratch_entry_only (); + server_updated (finfo, + NULL, + + /* Doesn't matter, it won't get checked. */ + SERVER_UPDATED, + + (mode_t) -1, + (unsigned char *) NULL, + (struct buffer *) NULL); + } +#endif + } + + /* Clearly this is right for T_MODIFIED. I haven't thought so much + about T_ADDED or T_REMOVED. */ + notify_do ('C', finfo->file, getcaller (), NULL, NULL, finfo->repository); + +out: + if (err != 0) + { + /* on failure, remove the file from ulist */ + p = findnode (ulist, finfo->file); + if (p) + delnode (p); + } + else + { + /* On success, retrieve the new version number of the file and + copy it into the log information (see logmsg.c + (logfile_write) for more details). We should only update + the version number for files that have been added or + modified but not removed since classify_file_internal + will return the version number of a file even after it has + been removed from the archive, which is not the behavior we + want for our commitlog messages; we want the old version + number and then "NONE." */ + + if (ci->status != T_REMOVED) + { + p = findnode (ulist, finfo->file); + if (p) + { + Vers_TS *vers; + struct logfile_info *li; + + (void) classify_file_internal (finfo, &vers); + li = p->data; + li->rev_new = xstrdup (vers->vn_rcs); + freevers_ts (&vers); + } + } + } + if (SIG_inCrSect ()) + SIG_endCrSect (); + + return err; +} + + + +/* + * Log the commit and clean up the update list + */ +/* ARGSUSED */ +static int +commit_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + const char *repository; + const char *update_dir; + List *entries; +{ + Node *p; + List *ulist; + + assert (repository); + + p = findnode (mulist, update_dir); + if (p == NULL) + return err; + + ulist = ((struct master_lists *) p->data)->ulist; + + got_message = 0; + + Update_Logfile (repository, saved_message, (FILE *) 0, ulist); + + /* Build the administrative files if necessary. */ + { + const char *p; + + if (strncmp (current_parsed_root->directory, repository, + strlen (current_parsed_root->directory)) != 0) + error (0, 0, + "internal error: repository (%s) doesn't begin with root (%s)", + repository, current_parsed_root->directory); + p = repository + strlen (current_parsed_root->directory); + if (*p == '/') + ++p; + if (strcmp ("CVSROOT", p) == 0 + /* Check for subdirectories because people may want to create + subdirectories and list files therein in checkoutlist. */ + || strncmp ("CVSROOT/", p, strlen ("CVSROOT/")) == 0 + ) + { + /* "Database" might a little bit grandiose and/or vague, + but "checked-out copies of administrative files, unless + in the case of modules and you are using ndbm in which + case modules.{pag,dir,db}" is verbose and excessively + focused on how the database is implemented. */ + + /* mkmodules requires the absolute name of the CVSROOT directory. + Remove anything after the `CVSROOT' component -- this is + necessary when committing in a subdirectory of CVSROOT. */ + char *admin_dir = xstrdup (repository); + int cvsrootlen = strlen ("CVSROOT"); + assert (admin_dir[p - repository + cvsrootlen] == '\0' + || admin_dir[p - repository + cvsrootlen] == '/'); + admin_dir[p - repository + cvsrootlen] = '\0'; + + cvs_output (program_name, 0); + cvs_output (" ", 1); + cvs_output (cvs_cmd_name, 0); + cvs_output (": Rebuilding administrative file database\n", 0); + mkmodules (admin_dir); + free (admin_dir); + } + } + + return err; +} + + + +/* + * Get the log message for a dir + */ +/* ARGSUSED */ +static Dtype +commit_direntproc (callerdat, dir, repos, update_dir, entries) + void *callerdat; + const char *dir; + const char *repos; + const char *update_dir; + List *entries; +{ + Node *p; + List *ulist; + char *real_repos; + + if (!isdir (dir)) + return R_SKIP_ALL; + + /* find the update list for this dir */ + p = findnode (mulist, update_dir); + if (p != NULL) + ulist = ((struct master_lists *) p->data)->ulist; + else + ulist = (List *) NULL; + + /* skip the files as an optimization */ + if (ulist == NULL || ulist->list->next == ulist->list) + return R_SKIP_FILES; + + /* get commit message */ + real_repos = Name_Repository (dir, update_dir); + got_message = 1; + if (!server_active && use_editor) + do_editor (update_dir, &saved_message, real_repos, ulist); + do_verify (&saved_message, real_repos); + free (real_repos); + return R_PROCESS; +} + + + +/* + * Process the post-commit proc if necessary + */ +/* ARGSUSED */ +static int +commit_dirleaveproc (callerdat, dir, err, update_dir, entries) + void *callerdat; + const char *dir; + int err; + const char *update_dir; + List *entries; +{ + /* update the per-directory tag info */ + /* FIXME? Why? The "commit examples" node of cvs.texinfo briefly + mentions commit -r being sticky, but apparently in the context of + this being a confusing feature! */ + if (err == 0 && write_dirtag != NULL) + { + char *repos = Name_Repository (NULL, update_dir); + WriteTag (NULL, write_dirtag, NULL, write_dirnonbranch, + update_dir, repos); + free (repos); + } + + return err; +} + + + +/* + * find the maximum major rev number in an entries file + */ +static int +findmaxrev (p, closure) + Node *p; + void *closure; +{ + int thisrev; + Entnode *entdata = p->data; + + if (entdata->type != ENT_FILE) + return 0; + thisrev = atoi (entdata->version); + if (thisrev > maxrev) + maxrev = thisrev; + return 0; +} + +/* + * Actually remove a file by moving it to the attic + * XXX - if removing a ,v file that is a relative symbolic link to + * another ,v file, we probably should add a ".." component to the + * link to keep it relative after we move it into the attic. + + Return value is 0 on success, or >0 on error (in which case we have + printed an error message). */ +static int +remove_file (finfo, tag, message) + struct file_info *finfo; + char *tag; + char *message; +{ + int retcode; + + int branch; + int lockflag; + char *corev; + char *rev; + char *prev_rev; + char *old_path; + + corev = NULL; + rev = NULL; + prev_rev = NULL; + + retcode = 0; + + if (finfo->rcs == NULL) + error (1, 0, "internal error: no parsed RCS file"); + + branch = 0; + if (tag && !(branch = RCS_nodeisbranch (finfo->rcs, tag))) + { + /* a symbolic tag is specified; just remove the tag from the file */ + if ((retcode = RCS_deltag (finfo->rcs, tag)) != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to remove tag `%s' from `%s'", tag, + finfo->fullname); + return 1; + } + RCS_rewrite (finfo->rcs, NULL, NULL); + Scratch_Entry (finfo->entries, finfo->file); + return 0; + } + + /* we are removing the file from either the head or a branch */ + /* commit a new, dead revision. */ + + /* Print message indicating that file is going to be removed. */ + cvs_output ("Removing ", 0); + cvs_output (finfo->fullname, 0); + cvs_output (";\n", 0); + + rev = NULL; + lockflag = 1; + if (branch) + { + char *branchname; + + rev = RCS_whatbranch (finfo->rcs, tag); + if (rev == NULL) + { + error (0, 0, "cannot find branch \"%s\".", tag); + return 1; + } + + branchname = RCS_getbranch (finfo->rcs, rev, 1); + if (branchname == NULL) + { + /* no revision exists on this branch. use the previous + revision but do not lock. */ + corev = RCS_gettag (finfo->rcs, tag, 1, (int *) NULL); + prev_rev = xstrdup (corev); + lockflag = 0; + } else + { + corev = xstrdup (rev); + prev_rev = xstrdup (branchname); + free (branchname); + } + + } else /* Not a branch */ + { + /* Get current head revision of file. */ + prev_rev = RCS_head (finfo->rcs); + } + + /* if removing without a tag or a branch, then make sure the default + branch is the trunk. */ + if (!tag && !branch) + { + if (RCS_setbranch (finfo->rcs, NULL) != 0) + { + error (0, 0, "cannot change branch to default for %s", + finfo->fullname); + return 1; + } + RCS_rewrite (finfo->rcs, NULL, NULL); + } + + /* check something out. Generally this is the head. If we have a + particular rev, then name it. */ + retcode = RCS_checkout (finfo->rcs, finfo->file, rev ? corev : NULL, + (char *) NULL, (char *) NULL, RUN_TTY, + (RCSCHECKOUTPROC) NULL, (void *) NULL); + if (retcode != 0) + { + error (0, 0, + "failed to check out `%s'", finfo->fullname); + return 1; + } + + /* Except when we are creating a branch, lock the revision so that + we can check in the new revision. */ + if (lockflag) + { + if (RCS_lock (finfo->rcs, rev ? corev : NULL, 1) == 0) + RCS_rewrite (finfo->rcs, NULL, NULL); + } + + if (corev != NULL) + free (corev); + + retcode = RCS_checkin (finfo->rcs, finfo->file, message, rev, 0, + RCS_FLAGS_DEAD | RCS_FLAGS_QUIET); + if (retcode != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to commit dead revision for `%s'", finfo->fullname); + if (prev_rev != NULL) + free (prev_rev); + return 1; + } + /* At this point, the file has been committed as removed. We should + probably tell the history file about it */ + corev = rev ? RCS_getbranch (finfo->rcs, rev, 1) : RCS_head (finfo->rcs); + history_write ('R', NULL, corev, finfo->file, finfo->repository); + free (corev); + + if (rev != NULL) + free (rev); + + old_path = xstrdup (finfo->rcs->path); + if (!branch) + RCS_setattic (finfo->rcs, 1); + + /* Print message that file was removed. */ + cvs_output (old_path, 0); + cvs_output (" <-- ", 0); + cvs_output (finfo->file, 0); + cvs_output ("\nnew revision: delete; previous revision: ", 0); + cvs_output (prev_rev, 0); + cvs_output ("\ndone\n", 0); + free(prev_rev); + + free (old_path); + + Scratch_Entry (finfo->entries, finfo->file); + return 0; +} + + + +/* + * Do the actual checkin for added files + */ +static int +finaladd (finfo, rev, tag, options) + struct file_info *finfo; + char *rev; + char *tag; + char *options; +{ + int ret; + + ret = Checkin ('A', finfo, rev, tag, options, saved_message); + if (ret == 0) + { + char *tmp = xmalloc (strlen (finfo->file) + sizeof (CVSADM) + + sizeof (CVSEXT_LOG) + 10); + (void) sprintf (tmp, "%s/%s%s", CVSADM, finfo->file, CVSEXT_LOG); + if (unlink_file (tmp) < 0 + && !existence_error (errno)) + error (0, errno, "cannot remove %s", tmp); + free (tmp); + } + else if (finfo->rcs != NULL) + fixaddfile (finfo->rcs->path); + + (void) time (&last_register_time); + + return ret; +} + + + +/* + * Unlock an rcs file + */ +static void +unlockrcs (rcs) + RCSNode *rcs; +{ + int retcode; + + if ((retcode = RCS_unlock (rcs, NULL, 1)) != 0) + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not unlock %s", rcs->path); + else + RCS_rewrite (rcs, NULL, NULL); +} + + + +/* + * remove a partially added file. if we can parse it, leave it alone. + * + * FIXME: Every caller that calls this function can access finfo->rcs (the + * parsed RCSNode data), so we should be able to detect that the file needs + * to be removed without reparsing the file as we do below. + */ +static void +fixaddfile (rcs) + const char *rcs; +{ + RCSNode *rcsfile; + int save_really_quiet; + + save_really_quiet = really_quiet; + really_quiet = 1; + if ((rcsfile = RCS_parsercsfile (rcs)) == NULL) + { + if (unlink_file (rcs) < 0) + error (0, errno, "cannot remove %s", rcs); + } + else + freercsnode (&rcsfile); + really_quiet = save_really_quiet; +} + + + +/* + * put the branch back on an rcs file + */ +static void +fixbranch (rcs, branch) + RCSNode *rcs; + char *branch; +{ + int retcode; + + if (branch != NULL) + { + if ((retcode = RCS_setbranch (rcs, branch)) != 0) + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "cannot restore branch to %s for %s", branch, rcs->path); + RCS_rewrite (rcs, NULL, NULL); + } +} + + + +/* + * do the initial part of a file add for the named file. if adding + * with a tag, put the file in the Attic and point the symbolic tag + * at the committed revision. + * + * INPUTS + * file The name of the file in the workspace. + * repository The repository directory to expect to find FILE,v in. + * tag The name or rev num of the branch being added to, if any. + * options Any RCS keyword expansion options specified by the user. + * rcsnode A pointer to the pre-parsed RCSNode for this file, if the file + * exists in the repository. If this is NULL, assume the file + * does not yet exist. + * + * RETURNS + * 0 on success. + * 1 on errors, after printing any appropriate error messages. + * + * ERRORS + * This function will return an error when any of the following functions do: + * add_rcs_file + * RCS_setattic + * lock_RCS + * RCS_checkin + * RCS_parse (called to verify the newly created archive file) + * RCS_settag + */ + +static int +checkaddfile (file, repository, tag, options, rcsnode) + const char *file; + const char *repository; + const char *tag; + const char *options; + RCSNode **rcsnode; +{ + RCSNode *rcs; + char *fname; + int newfile = 0; /* Set to 1 if we created a new RCS archive. */ + int retval = 1; + int adding_on_branch; + + assert (rcsnode != NULL); + + /* Callers expect to be able to use either "" or NULL to mean the + default keyword expansion. */ + if (options != NULL && options[0] == '\0') + options = NULL; + if (options != NULL) + assert (options[0] == '-' && options[1] == 'k'); + + /* If numeric, it is on the trunk; check_fileproc enforced + this. */ + adding_on_branch = tag != NULL && !isdigit ((unsigned char) tag[0]); + + if (*rcsnode == NULL) + { + char *rcsname; + char *desc = NULL; + size_t descalloc = 0; + size_t desclen = 0; + const char *opt; + + if ( adding_on_branch ) + { + mode_t omask; + rcsname = xmalloc (strlen (repository) + + sizeof (CVSATTIC) + + strlen (file) + + sizeof (RCSEXT) + + 3); + (void) sprintf (rcsname, "%s/%s", repository, CVSATTIC); + omask = umask ( cvsumask ); + if (CVS_MKDIR (rcsname, 0777 ) != 0 && errno != EEXIST) + error (1, errno, "cannot make directory `%s'", rcsname); + (void) umask ( omask ); + (void) sprintf (rcsname, + "%s/%s/%s%s", + repository, + CVSATTIC, + file, + RCSEXT); + } + else + { + rcsname = xmalloc (strlen (repository) + + strlen (file) + + sizeof (RCSEXT) + + 2); + (void) sprintf (rcsname, + "%s/%s%s", + repository, + file, + RCSEXT); + } + + /* this is the first time we have ever seen this file; create + an RCS file. */ + fname = xmalloc (strlen (file) + sizeof (CVSADM) + + sizeof (CVSEXT_LOG) + 10); + (void) sprintf (fname, "%s/%s%s", CVSADM, file, CVSEXT_LOG); + /* If the file does not exist, no big deal. In particular, the + server does not (yet at least) create CVSEXT_LOG files. */ + if (isfile (fname)) + /* FIXME: Should be including update_dir in the appropriate + place here. */ + get_file (fname, fname, "r", &desc, &descalloc, &desclen); + free (fname); + + /* From reading the RCS 5.7 source, "rcs -i" adds a newline to the + end of the log message if the message is nonempty. + Do it. RCS also deletes certain whitespace, in cleanlogmsg, + which we don't try to do here. */ + if (desclen > 0) + { + expand_string (&desc, &descalloc, desclen + 1); + desc[desclen++] = '\012'; + } + + /* Set RCS keyword expansion options. */ + if (options != NULL) + opt = options + 2; + else + opt = NULL; + + /* This message is an artifact of the time when this + was implemented via "rcs -i". It should be revised at + some point (does the "initial revision" in the message from + RCS_checkin indicate that this is a new file? Or does the + "RCS file" message serve some function?). */ + cvs_output ("RCS file: ", 0); + cvs_output (rcsname, 0); + cvs_output ("\ndone\n", 0); + + if (add_rcs_file (NULL, rcsname, file, NULL, opt, + NULL, NULL, 0, NULL, + desc, desclen, NULL) != 0) + { + if (rcsname != NULL) + free (rcsname); + goto out; + } + rcs = RCS_parsercsfile (rcsname); + newfile = 1; + if (rcsname != NULL) + free (rcsname); + if (desc != NULL) + free (desc); + *rcsnode = rcs; + } + else + { + /* file has existed in the past. Prepare to resurrect. */ + char *rev; + char *oldexpand; + + rcs = *rcsnode; + + oldexpand = RCS_getexpand (rcs); + if ((oldexpand != NULL + && options != NULL + && strcmp (options + 2, oldexpand) != 0) + || (oldexpand == NULL && options != NULL)) + { + /* We tell the user about this, because it means that the + old revisions will no longer retrieve the way that they + used to. */ + error (0, 0, "changing keyword expansion mode to %s", options); + RCS_setexpand (rcs, options + 2); + } + + if (!adding_on_branch) + { + /* We are adding on the trunk, so move the file out of the + Attic. */ + if (!(rcs->flags & INATTIC)) + { + error (0, 0, "warning: expected %s to be in Attic", + rcs->path); + } + + /* Begin a critical section around the code that spans the + first commit on the trunk of a file that's already been + committed on a branch. */ + SIG_beginCrSect (); + + if (RCS_setattic (rcs, 0)) + { + goto out; + } + } + + rev = RCS_getversion (rcs, tag, NULL, 1, (int *) NULL); + /* and lock it */ + if (lock_RCS (file, rcs, rev, repository)) + { + error (0, 0, "cannot lock revision %s in `%s'.", + rev ? rev : tag ? tag : "HEAD", rcs->path); + if (rev != NULL) + free (rev); + goto out; + } + + if (rev != NULL) + free (rev); + } + + /* when adding a file for the first time, and using a tag, we need + to create a dead revision on the trunk. */ + if (adding_on_branch) + { + if (newfile) + { + char *tmp; + FILE *fp; + int retcode; + + /* move the new file out of the way. */ + fname = xmalloc (strlen (file) + sizeof (CVSADM) + + sizeof (CVSPREFIX) + 10); + (void) sprintf (fname, "%s/%s%s", CVSADM, CVSPREFIX, file); + rename_file (file, fname); + + /* Create empty FILE. Can't use copy_file with a DEVNULL + argument -- copy_file now ignores device files. */ + fp = fopen (file, "w"); + if (fp == NULL) + error (1, errno, "cannot open %s for writing", file); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", file); + + tmp = xmalloc (strlen (file) + strlen (tag) + 80); + /* commit a dead revision. */ + (void) sprintf (tmp, "file %s was initially added on branch %s.", + file, tag); + retcode = RCS_checkin (rcs, NULL, tmp, NULL, 0, + RCS_FLAGS_DEAD | RCS_FLAGS_QUIET); + free (tmp); + if (retcode != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not create initial dead revision %s", rcs->path); + free (fname); + goto out; + } + + /* put the new file back where it was */ + rename_file (fname, file); + free (fname); + + /* double-check that the file was written correctly */ + freercsnode (&rcs); + rcs = RCS_parse (file, repository); + if (rcs == NULL) + { + error (0, 0, "could not read %s in %s", file, repository); + goto out; + } + *rcsnode = rcs; + + /* and lock it once again. */ + if (lock_RCS (file, rcs, NULL, repository)) + { + error (0, 0, "cannot lock initial revision in `%s'.", + rcs->path); + goto out; + } + } + + /* when adding with a tag, we need to stub a branch, if it + doesn't already exist. */ + if (!RCS_nodeisbranch (rcs, tag)) + { + /* branch does not exist. Stub it. */ + char *head; + char *magicrev; + int retcode; + time_t headtime = -1; + char *revnum, *tmp; + FILE *fp; + time_t t = -1; + struct tm *ct; + + fixbranch (rcs, sbranch); + + head = RCS_getversion (rcs, NULL, NULL, 0, (int *) NULL); + if (!head) + error (1, 0, "No head revision in archive file `%s'.", + rcs->path); + magicrev = RCS_magicrev (rcs, head); + + /* If this is not a new branch, then we will want a dead + version created before this one. */ + if (!newfile) + headtime = RCS_getrevtime (rcs, head, 0, 0); + + retcode = RCS_settag (rcs, tag, magicrev); + RCS_rewrite (rcs, NULL, NULL); + + free (head); + free (magicrev); + + if (retcode != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not stub branch %s for %s", tag, rcs->path); + goto out; + } + /* We need to add a dead version here to avoid -rtag -Dtime + checkout problems between when the head version was + created and now. */ + if (!newfile && headtime != -1) + { + /* move the new file out of the way. */ + fname = xmalloc (strlen (file) + sizeof (CVSADM) + + sizeof (CVSPREFIX) + 10); + (void) sprintf (fname, "%s/%s%s", CVSADM, CVSPREFIX, file); + rename_file (file, fname); + + /* Create empty FILE. Can't use copy_file with a DEVNULL + argument -- copy_file now ignores device files. */ + fp = fopen (file, "w"); + if (fp == NULL) + error (1, errno, "cannot open %s for writing", file); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", file); + + /* As we will be hacking the delta date, put the time + this was added into the log message. */ + t = time(NULL); + ct = gmtime(&t); + tmp = xmalloc (strlen (file) + strlen (tag) + 80); + + (void) sprintf (tmp, + "file %s was added on branch %s on %d-%02d-%02d %02d:%02d:%02d +0000", + file, tag, + ct->tm_year + (ct->tm_year < 100 ? 0 : 1900), + ct->tm_mon + 1, ct->tm_mday, + ct->tm_hour, ct->tm_min, ct->tm_sec); + + /* commit a dead revision. */ + revnum = RCS_whatbranch (rcs, tag); + retcode = RCS_checkin (rcs, NULL, tmp, revnum, headtime, + RCS_FLAGS_DEAD | + RCS_FLAGS_QUIET | + RCS_FLAGS_USETIME); + free (revnum); + free (tmp); + + if (retcode != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not created dead stub %s for %s", tag, + rcs->path); + goto out; + } + + /* put the new file back where it was */ + rename_file (fname, file); + free (fname); + + /* double-check that the file was written correctly */ + freercsnode (&rcs); + rcs = RCS_parse (file, repository); + if (rcs == NULL) + { + error (0, 0, "could not read %s", rcs->path); + goto out; + } + *rcsnode = rcs; + } + } + else + { + /* lock the branch. (stubbed branches need not be locked.) */ + if (lock_RCS (file, rcs, NULL, repository)) + { + error (0, 0, "cannot lock head revision in `%s'.", rcs->path); + goto out; + } + } + + if (*rcsnode != rcs) + { + freercsnode(rcsnode); + *rcsnode = rcs; + } + } + + fileattr_newfile (file); + + /* At this point, we used to set the file mode of the RCS file + based on the mode of the file in the working directory. If we + are creating the RCS file for the first time, add_rcs_file does + this already. If we are re-adding the file, then perhaps it is + consistent to preserve the old file mode, just as we preserve + the old keyword expansion mode. + + If we decide that we should change the modes, then we can't do + it here anyhow. At this point, the RCS file may be owned by + somebody else, so a chmod will fail. We need to instead do the + chmod after rewriting it. + + FIXME: In general, I think the file mode (and the keyword + expansion mode) should be associated with a particular revision + of the file, so that it is possible to have different revisions + of a file have different modes. */ + + retval = 0; + + out: + if (retval != 0 && SIG_inCrSect ()) + SIG_endCrSect (); + return retval; +} + + + +/* + * Attempt to place a lock on the RCS file; returns 0 if it could and 1 if it + * couldn't. If the RCS file currently has a branch as the head, we must + * move the head back to the trunk before locking the file, and be sure to + * put the branch back as the head if there are any errors. + */ +static int +lock_RCS (user, rcs, rev, repository) + const char *user; + RCSNode *rcs; + const char *rev; + const char *repository; +{ + char *branch = NULL; + int err = 0; + + /* + * For a specified, numeric revision of the form "1" or "1.1", (or when + * no revision is specified ""), definitely move the branch to the trunk + * before locking the RCS file. + * + * The assumption is that if there is more than one revision on the trunk, + * the head points to the trunk, not a branch... and as such, it's not + * necessary to move the head in this case. + */ + if (rev == NULL + || (rev && isdigit ((unsigned char) *rev) && numdots (rev) < 2)) + { + branch = xstrdup (rcs->branch); + if (branch != NULL) + { + if (RCS_setbranch (rcs, NULL) != 0) + { + error (0, 0, "cannot change branch to default for %s", + rcs->path); + if (branch) + free (branch); + return 1; + } + } + err = RCS_lock (rcs, NULL, 1); + } + else + { + RCS_lock (rcs, rev, 1); + } + + /* We used to call RCS_rewrite here, and that might seem + appropriate in order to write out the locked revision + information. However, such a call would actually serve no + purpose. CVS locks will prevent any interference from other + CVS processes. The comment above rcs_internal_lockfile + explains that it is already unsafe to use RCS and CVS + simultaneously. It follows that writing out the locked + revision information here would add no additional security. + + If we ever do care about it, the proper fix is to create the + RCS lock file before calling this function, and maintain it + until the checkin is complete. + + The call to RCS_lock is still required at present, since in + some cases RCS_checkin will determine which revision to check + in by looking for a lock. FIXME: This is rather roundabout, + and a more straightforward approach would probably be easier to + understand. */ + + if (err == 0) + { + if (sbranch != NULL) + free (sbranch); + sbranch = branch; + return 0; + } + + /* try to restore the branch if we can on error */ + if (branch != NULL) + fixbranch (rcs, branch); + + if (branch) + free (branch); + return 1; +} + + + +/* + * free an UPDATE node's data + */ +void +update_delproc (p) + Node *p; +{ + struct logfile_info *li = p->data; + + if (li->tag) + free (li->tag); + if (li->rev_old) + free (li->rev_old); + if (li->rev_new) + free (li->rev_new); + free (li); +} + +/* + * Free the commit_info structure in p. + */ +static void +ci_delproc (p) + Node *p; +{ + struct commit_info *ci = p->data; + + if (ci->rev) + free (ci->rev); + if (ci->tag) + free (ci->tag); + if (ci->options) + free (ci->options); + free (ci); +} + +/* + * Free the commit_info structure in p. + */ +static void +masterlist_delproc (p) + Node *p; +{ + struct master_lists *ml = p->data; + + dellist (&ml->ulist); + dellist (&ml->cilist); + free (ml); +} |