Current Path : /usr/src/contrib/cvs/src/ |
FreeBSD hs32.drive.ne.jp 9.1-RELEASE FreeBSD 9.1-RELEASE #1: Wed Jan 14 12:18:08 JST 2015 root@hs32.drive.ne.jp:/sys/amd64/compile/hs32 amd64 |
Current File : //usr/src/contrib/cvs/src/commit.c |
/* * 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: release/9.1.0/contrib/cvs/src/commit.c 175282 2008-01-13 06:20:11Z obrien $ */ #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); }