diff options
Diffstat (limited to 'contrib/cvs/src/commit.c')
-rw-r--r-- | contrib/cvs/src/commit.c | 1824 |
1 files changed, 1824 insertions, 0 deletions
diff --git a/contrib/cvs/src/commit.c b/contrib/cvs/src/commit.c new file mode 100644 index 0000000..ac81790 --- /dev/null +++ b/contrib/cvs/src/commit.c @@ -0,0 +1,1824 @@ +/* + * Copyright (c) 1992, Brian Berliner and Jeff Polk + * Copyright (c) 1989-1992, Brian Berliner + * + * You may distribute under the terms of the GNU General Public License as + * specified in the README file that comes with the CVS 1.4 kit. + * + * 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... + * + */ + +#include "cvs.h" +#include "getline.h" +#include "edit.h" +#include "fileattr.h" + +static Dtype check_direntproc PROTO((char *dir, char *repos, char *update_dir)); +static int check_fileproc PROTO((struct file_info *finfo)); +static int check_filesdoneproc PROTO((int err, char *repos, char *update_dir)); +static int checkaddfile PROTO((char *file, char *repository, char *tag, + char *options, RCSNode **rcsnode)); +static Dtype commit_direntproc PROTO((char *dir, char *repos, char *update_dir)); +static int commit_dirleaveproc PROTO((char *dir, int err, char *update_dir)); +static int commit_fileproc PROTO((struct file_info *finfo)); +static int commit_filesdoneproc PROTO((int err, char *repository, char *update_dir)); +static int finaladd PROTO((char *file, char *revision, char *tag, + char *options, char *update_dir, + char *repository, List *entries)); +static int findmaxrev PROTO((Node * p, void *closure)); +static int lock_RCS PROTO((char *user, char *rcs, char *rev, char *repository)); +static int lockrcsfile PROTO((char *file, char *repository, char *rev)); +static int precommit_list_proc PROTO((Node * p, void *closure)); +static int precommit_proc PROTO((char *repository, char *filter)); +static int remove_file PROTO((char *file, char *repository, char *tag, + char *message, List *entries, RCSNode *rcsnode)); +static void fix_rcs_modes PROTO((char *rcs, char *user)); +static void fixaddfile PROTO((char *file, char *repository)); +static void fixbranch PROTO((char *file, char *repository, char *branch)); +static void unlockrcs PROTO((char *file, char *repository)); +static void ci_delproc PROTO((Node *p)); +static void masterlist_delproc PROTO((Node *p)); +static void locate_rcs PROTO((char *file, char *repository, char *rcs)); + +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 run_module_prog = 1; +static int aflag; +static char *tag; +static char *write_dirtag; +static char *logfile; +static List *mulist; +static char *message; +static time_t last_register_time; + + +static const char *const commit_usage[] = +{ + "Usage: %s %s [-nRlf] [-m msg | -F logfile] [-r rev] files...\n", + "\t-n\tDo not run the module program (if any).\n", + "\t-R\tProcess directories recursively.\n", + "\t-l\tLocal directory only (not recursive).\n", + "\t-f\tForce the file to be committed; disables recursion.\n", + "\t-F file\tRead the log message from file.\n", + "\t-m msg\tLog message.\n", + "\t-r rev\tCommit to this branch or trunk revision.\n", + NULL +}; + +#ifdef CLIENT_SUPPORT +struct find_data { + List *ulist; + int argc; + char **argv; +}; + +/* Pass as a static until we get around to fixing start_recursion to + pass along a void * where we can stash it. */ +struct find_data *find_data_static; + +static int find_fileproc PROTO ((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 (finfo) + struct file_info *finfo; +{ + Vers_TS *vers; + enum classify_type status; + Node *node; + struct find_data *args = find_data_static; + + vers = Version_TS ((char *)NULL, (char *)NULL, (char *)NULL, + (char *)NULL, + finfo->file, 0, 0, finfo->entries, (RCSNode *)NULL); + if (vers->ts_user == NULL + && vers->vn_user != NULL + && vers->vn_user[0] == '-') + status = T_REMOVED; + else if (vers->vn_user == NULL) + { + if (vers->ts_user == NULL) + error (0, 0, "nothing known about `%s'", finfo->fullname); + else + error (0, 0, "use `cvs add' to create an entry for %s", + finfo->fullname); + return 1; + } + else if (vers->ts_user != NULL + && vers->vn_user != NULL + && vers->vn_user[0] == '0') + status = T_ADDED; + else if (vers->ts_user != NULL + && vers->ts_rcs != NULL + && strcmp (vers->ts_user, vers->ts_rcs) != 0) + 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). */ + return 0; + } + + node = getnode (); + node->key = xstrdup (finfo->fullname); + + node->type = UPDATE; + node->delproc = update_delproc; + node->data = (char *) status; + (void)addnode (args->ulist, node); + + ++args->argc; + + 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 */ + +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. + */ + if (geteuid () == (uid_t) 0) + { + struct passwd *pw; + + if ((pw = (struct passwd *) getpwnam (getcaller ())) == NULL) + error (1, 0, "you are unknown to this system"); + if (pw->pw_uid == (uid_t) 0) + error (1, 0, "cannot commit files as 'root'"); + } +#endif /* CVS_BADROOT */ + + optind = 1; + while ((c = getopt (argc, argv, "nlRm:fF:r:")) != -1) + { + switch (c) + { + case 'n': + run_module_prog = 0; + break; + case 'm': +#ifdef FORCE_USE_EDITOR + use_editor = TRUE; +#else + use_editor = FALSE; +#endif + if (message) + { + free (message); + message = NULL; + } + + message = xstrdup(optarg); + break; + case 'r': + if (tag) + free (tag); + 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 = TRUE; +#else + use_editor = FALSE; +#endif + logfile = optarg; + break; + case '?': + default: + usage (commit_usage); + break; + } + } + argc -= optind; + argv += optind; + + /* numeric specified revision means we ignore sticky tags... */ + if (tag && isdigit (*tag)) + { + aflag = 1; + /* strip trailing dots */ + while (tag[strlen (tag) - 1] == '.') + tag[strlen (tag) - 1] = '\0'; + } + + /* some checks related to the "-F logfile" option */ + if (logfile) + { + int n, logfd; + struct stat statbuf; + + if (message) + error (1, 0, "cannot specify both a message and a log file"); + + /* FIXME: Why is this binary? Needs more investigation. */ + if ((logfd = open (logfile, O_RDONLY | OPEN_BINARY)) < 0) + error (1, errno, "cannot open log file %s", logfile); + + if (fstat(logfd, &statbuf) < 0) + error (1, errno, "cannot find size of log file %s", logfile); + + message = xmalloc (statbuf.st_size + 1); + + /* FIXME: Should keep reading until EOF, rather than assuming the + first read gets the whole thing. */ + if ((n = read (logfd, message, statbuf.st_size + 1)) < 0) + error (1, errno, "cannot read log message from %s", logfile); + + (void) close (logfd); + message[n] = '\0'; + } + +#ifdef CLIENT_SUPPORT + if (client_active) + { + struct find_data find_args; + + ign_setup (); + + /* Note that we don't do ignore file processing here, and we + don't call ignore_files. This means that we won't print "? + foo" for stray files. Sounds OK, the doc only promises + that update does that. */ + find_args.ulist = getlist (); + find_args.argc = 0; + find_data_static = &find_args; + err = start_recursion (find_fileproc, (FILESDONEPROC) NULL, + (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, + argc, argv, local, W_LOCAL, 0, 0, + (char *)NULL, 0, 0); + if (err) + error (1, 0, "correct above errors first!"); + + if (find_args.argc == 0) + 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. */ + find_args.argv = (char **) xmalloc (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.). */ + 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 (".", &message, (char *)NULL, find_args.ulist); + + /* We always send some sort of message, even if empty. */ + option_with_arg ("-m", message); + + if (local) + send_arg("-l"); + if (force_ci) + send_arg("-f"); + if (!run_module_prog) + send_arg("-n"); + option_with_arg ("-r", tag); + + /* 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); + send_files (find_args.argc, find_args.argv, local, 0); + + send_to_server ("ci\012", 0); + return get_responses_and_close (); + } +#endif + + if (tag != NULL) + tag_check_valid (tag, argc, argv, local, aflag, ""); + + /* XXX - this is not the perfect check for this */ + if (argc <= 0) + write_dirtag = tag; + + wrap_setup (); + + lock_tree_for_write (argc, argv, local, aflag); + + /* + * Set up the master update list + */ + mulist = getlist (); + + /* + * Run the recursion processor to verify the files are all up-to-date + */ + err = start_recursion (check_fileproc, check_filesdoneproc, + check_direntproc, (DIRLEAVEPROC) NULL, argc, + argv, local, W_LOCAL, aflag, 0, (char *) NULL, 1, + 0); + if (err) + { + lock_tree_cleanup (); + error (1, 0, "correct above errors first!"); + } + + /* + * Run the recursion processor to commit the files + */ + if (noexec == 0) + err = start_recursion (commit_fileproc, commit_filesdoneproc, + commit_direntproc, commit_dirleaveproc, + argc, argv, local, W_LOCAL, aflag, 0, + (char *) NULL, 1, 0); + + /* + * Unlock all the dirs and clean up + */ + lock_tree_cleanup (); + dellist (&mulist); + + if (last_register_time) + { + time_t now; + + (void) time (&now); + if (now == last_register_time) + { + sleep (1); /* to avoid time-stamp races */ + } + } + + return (err); +} + +/* + * Check to see if a file is ok to commit and make sure all files are + * up-to-date + */ +/* ARGSUSED */ +static int +check_fileproc (finfo) + struct file_info *finfo; +{ + Ctype status; + char *xdir; + Node *p; + List *ulist, *cilist; + Vers_TS *vers; + struct commit_info *ci; + int save_noexec, save_quiet, save_really_quiet; + + save_noexec = noexec; + save_quiet = quiet; + save_really_quiet = really_quiet; + noexec = quiet = really_quiet = 1; + + /* handle specified numeric revision specially */ + if (tag && isdigit (*tag)) + { + /* If the tag is for the trunk, make sure we're at the head */ + if (numdots (tag) < 2) + { + status = Classify_File (finfo->file, (char *) NULL, (char *) NULL, + (char *) NULL, 1, aflag, finfo->repository, + finfo->entries, finfo->rcs, &vers, finfo->update_dir, 0); + if (status == T_UPTODATE || status == T_MODIFIED || + status == T_ADDED) + { + Ctype xstatus; + + freevers_ts (&vers); + xstatus = Classify_File (finfo->file, tag, (char *) NULL, + (char *) NULL, 1, aflag, finfo->repository, + finfo->entries, finfo->rcs, &vers, finfo->update_dir, + 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 (tag); + if ((numdots (xtag) & 1) != 0) + { + cp = strrchr (xtag, '.'); + *cp = '\0'; + } + status = Classify_File (finfo->file, xtag, (char *) NULL, + (char *) NULL, 1, aflag, finfo->repository, + finfo->entries, finfo->rcs, &vers, finfo->update_dir, 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->file, xtag, (char *) NULL, + (char *) NULL, 1, aflag, finfo->repository, + finfo->entries, finfo->rcs, &vers, finfo->update_dir, + 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 (tag); + free (xtag); + } + } + else + status = Classify_File (finfo->file, tag, (char *) NULL, (char *) NULL, + 1, 0, finfo->repository, finfo->entries, finfo->rcs, &vers, + finfo->update_dir, 0); + noexec = save_noexec; + quiet = save_quiet; + really_quiet = save_really_quiet; + + /* + * 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: +#ifdef SERVER_SUPPORT + case T_PATCH: +#endif + case T_NEEDS_MERGE: + case T_CONFLICT: + case T_REMOVE_ENTRY: + error (0, 0, "Up-to-date check failed for `%s'", finfo->fullname); + freevers_ts (&vers); + return (1); + 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, can't have a numeric tag + * - if status is T_ADDED, rcs file must not exist + * - 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 (!tag || !isdigit (*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_MODIFIED && !force_ci && vers->ts_conflict) + { + char *filestamp; + int retcode; + + /* + * We found a "conflict" marker. + * + * If the timestamp on the file is the same as the + * timestamp stored in the Entries file, we block the commit. + */ +#ifdef SERVER_SUPPORT + if (server_active) + retcode = vers->ts_conflict[0] != '='; + else { + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); + } +#else + filestamp = time_stamp (finfo->file); + retcode = strcmp (vers->ts_conflict, filestamp); + free (filestamp); +#endif + if (retcode == 0) + { + error (0, 0, + "file `%s' had a conflict and has not been modified", + finfo->fullname); + freevers_ts (&vers); + return (1); + } + + /* + * If the timestamps differ, look for Conflict indicators + * in the file to see if we should block the commit anyway + */ + run_setup ("%s", GREP); + run_arg (RCS_MERGE_PAT); + run_arg (finfo->file); + retcode = run_exec (RUN_TTY, DEVNULL, RUN_TTY, RUN_REALLY); + + if (retcode == -1) + { + error (1, errno, + "fork failed while examining conflict in `%s'", + finfo->fullname); + } + else if (retcode == 0) + { + error (0, 0, + "file `%s' still contains conflict indicators", + finfo->fullname); + freevers_ts (&vers); + return (1); + } + } + + if (status == T_REMOVED && vers->tag && isdigit (*vers->tag)) + { + 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) + { + char rcs[PATH_MAX]; + + /* Don't look in the attic; if it exists there we will + move it back out in checkaddfile. */ + sprintf(rcs, "%s/%s%s", finfo->repository, finfo->file, RCSEXT); + if (isreadable (rcs)) + { + error (0, 0, + "cannot add file `%s' when RCS file `%s' already exists", + finfo->fullname, rcs); + freevers_ts (&vers); + return (1); + } + if (vers->tag && isdigit (*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 = (char *) 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; + p->data = (char *) status; + (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 (*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 = (char *) ci; + (void) addnode (cilist, p); + 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); +} + +/* + * Print warm fuzzies while examining the dirs + */ +/* ARGSUSED */ +static Dtype +check_direntproc (dir, repos, update_dir) + char *dir; + char *repos; + char *update_dir; +{ + 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; +{ + if (p->data == (char *) T_ADDED || p->data == (char *) T_MODIFIED || + p->data == (char *) T_REMOVED) + { + run_arg (p->key); + } + return (0); +} + +/* + * Callback proc for pre-commit checking + */ +static List *ulist; +static int +precommit_proc (repository, filter) + char *repository; + 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 (*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 ("%s %s", filter, repository); + (void) walklist (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 (err, repos, update_dir) + int err; + char *repos; + char *update_dir; +{ + int n; + Node *p; + + /* 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 checks if there's nothing to do */ + if (ulist == NULL || ulist->list->next == 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[PATH_MAX]; + +/* ARGSUSED */ +static int +commit_fileproc (finfo) + struct file_info *finfo; +{ + Node *p; + int err = 0; + List *ulist, *cilist; + struct commit_info *ci; + char rcs[PATH_MAX]; + + 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 (use_editor && !got_message) + { + got_message = 1; + do_editor (finfo->update_dir, &message, finfo->repository, ulist); + } + + p = findnode (cilist, finfo->file); + if (p == NULL) + return (0); + + ci = (struct commit_info *) p->data; + if (ci->status == T_MODIFIED) + { + if (lockrcsfile (finfo->file, finfo->repository, ci->rev) != 0) + { + unlockrcs (finfo->file, finfo->repository); + err = 1; + goto out; + } + } + else if (ci->status == T_ADDED) + { + if (checkaddfile (finfo->file, finfo->repository, ci->tag, ci->options, + &finfo->rcs) != 0) + { + fixaddfile (finfo->file, finfo->repository); + 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) { + locate_rcs (finfo->file, finfo->repository, rcs); + ci->rev = RCS_whatbranch (finfo->rcs, ci->tag); + err = Checkin ('A', finfo->file, finfo->update_dir, finfo->repository, rcs, ci->rev, + ci->tag, ci->options, message, finfo->entries); + if (err != 0) + { + unlockrcs (finfo->file, finfo->repository); + fixbranch (finfo->file, finfo->repository, 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 (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->file, ci->rev ? ci->rev : xrev, ci->tag, ci->options, + finfo->update_dir, finfo->repository, finfo->entries); + if (xrev) + free (xrev); + } + else if (ci->status == T_MODIFIED) + { + locate_rcs (finfo->file, finfo->repository, rcs); + err = Checkin ('M', finfo->file, finfo->update_dir, finfo->repository, + rcs, ci->rev, ci->tag, + ci->options, message, finfo->entries); + + (void) time (&last_register_time); + + if (err != 0) + { + unlockrcs (finfo->file, finfo->repository); + fixbranch (finfo->file, finfo->repository, sbranch); + } + } + else if (ci->status == T_REMOVED) + { + err = remove_file (finfo->file, finfo->repository, ci->tag, message, + finfo->entries, finfo->rcs); +#ifdef SERVER_SUPPORT + if (server_active) { + server_scratch_entry_only (); + server_updated (finfo->file, finfo->update_dir, finfo->repository, + /* Doesn't matter, it won't get checked. */ + SERVER_UPDATED, (struct stat *) NULL, + (unsigned char *) 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); + } + + return (err); +} + +/* + * Log the commit and clean up the update list + */ +/* ARGSUSED */ +static int +commit_filesdoneproc (err, repository, update_dir) + int err; + char *repository; + char *update_dir; +{ + char *xtag = (char *) NULL; + Node *p; + List *ulist; + + p = findnode (mulist, update_dir); + if (p == NULL) + return (err); + + ulist = ((struct master_lists *) p->data)->ulist; + + got_message = 0; + + /* see if we need to specify a per-directory or -r option tag */ + if (tag == NULL) + ParseTag (&xtag, (char **) NULL); + + Update_Logfile (repository, message, tag ? tag : xtag, (FILE *) 0, ulist); + if (xtag) + free (xtag); + + /* Build the administrative files if necessary. */ + { + char *p; + + if (strncmp (CVSroot, repository, strlen (CVSroot)) != 0) + error (0, 0, "internal error: repository doesn't begin with root"); + p = repository + strlen (CVSroot); + if (*p == '/') + ++p; + if (strcmp ("CVSROOT", p) == 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. */ + + cvs_output (program_name, 0); + cvs_output (" ", 1); + cvs_output (command_name, 0); + cvs_output (": Rebuilding administrative file database\n", 0); + mkmodules (repository); + } + } + + if (err == 0 && run_module_prog) + { + FILE *fp; + + if ((fp = fopen (CVSADM_CIPROG, "r")) != NULL) + { + char *line; + int line_length; + size_t line_chars_allocated; + char *repository; + + line = NULL; + line_chars_allocated = 0; + line_length = getline (&line, &line_chars_allocated, fp); + if (line_length > 0) + { + /* Remove any trailing newline. */ + if (line[line_length - 1] == '\n') + line[--line_length] = '\0'; + repository = Name_Repository ((char *) NULL, update_dir); + run_setup ("%s %s", line, repository); + (void) printf ("%s %s: Executing '", program_name, + command_name); + run_print (stdout); + (void) printf ("'\n"); + (void) run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL); + free (repository); + } + else + { + if (ferror (fp)) + error (0, errno, "warning: error reading %s", + CVSADM_CIPROG); + } + if (line != NULL) + free (line); + if (fclose (fp) < 0) + error (0, errno, "warning: cannot close %s", CVSADM_CIPROG); + } + else + { + if (! existence_error (errno)) + error (0, errno, "warning: cannot open %s", CVSADM_CIPROG); + } + } + + return (err); +} + +/* + * Get the log message for a dir and print a warm fuzzy + */ +/* ARGSUSED */ +static Dtype +commit_direntproc (dir, repos, update_dir) + char *dir; + char *repos; + char *update_dir; +{ + Node *p; + List *ulist; + char *real_repos; + + /* 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); + + /* print the warm fuzzy */ + if (!quiet) + error (0, 0, "Committing %s", update_dir); + + /* get commit message */ + if (use_editor) + { + got_message = 1; + real_repos = Name_Repository (dir, update_dir); + do_editor (update_dir, &message, real_repos, ulist); + free (real_repos); + } + return (R_PROCESS); +} + +/* + * Process the post-commit proc if necessary + */ +/* ARGSUSED */ +static int +commit_dirleaveproc (dir, err, update_dir) + char *dir; + int err; + char *update_dir; +{ + /* update the per-directory tag info */ + if (err == 0 && write_dirtag != NULL) + { + WriteTag ((char *) NULL, write_dirtag, (char *) NULL); +#ifdef SERVER_SUPPORT + if (server_active) + server_set_sticky (update_dir, Name_Repository (dir, update_dir), + write_dirtag, (char *) NULL); +#endif + } + + return (err); +} + +/* + * find the maximum major rev number in an entries file + */ +static int +findmaxrev (p, closure) + Node *p; + void *closure; +{ + char *cp; + int thisrev; + Entnode *entdata; + + entdata = (Entnode *) p->data; + cp = strchr (entdata->version, '.'); + if (cp != NULL) + *cp = '\0'; + thisrev = atoi (entdata->version); + if (cp != NULL) + *cp = '.'; + 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. + */ +static int +remove_file (file, repository, tag, message, entries, rcsnode) + char *file; + char *repository; + char *tag; + char *message; + List *entries; + RCSNode *rcsnode; +{ + mode_t omask; + int retcode; + char rcs[PATH_MAX]; + char *tmp; + + int branch; + int lockflag; + char *corev; + char *rev; + char *prev_rev; + + corev = NULL; + rev = NULL; + prev_rev = NULL; + + retcode = 0; + + locate_rcs (file, repository, rcs); + + branch = 0; + if (tag && !(branch = RCS_isbranch (rcsnode, tag))) + { + /* a symbolic tag is specified; just remove the tag from the file */ + if ((retcode = RCS_deltag (rcs, tag, 1)) != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to remove tag `%s' from `%s'", tag, rcs); + return (1); + } + Scratch_Entry (entries, 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. */ + (void) printf ("Removing %s;\n", file); + + rev = NULL; + lockflag = RCS_FLAGS_LOCK; + if (branch) + { + char *branchname; + + rev = RCS_whatbranch (rcsnode, tag); + if (rev == NULL) + { + error (0, 0, "cannot find branch \"%s\".", tag); + return (1); + } + + if (rcsnode == NULL) + { + error (0, 0, "boy, I'm confused."); + return (1); + } + branchname = RCS_getbranch (rcsnode, rev, 1); + if (branchname == NULL) + { + /* no revision exists on this branch. use the previous + revision but do not lock. */ + corev = RCS_gettag (rcsnode, tag, 1, 0); + prev_rev = xstrdup(rev); + lockflag = 0; + } else + { + corev = xstrdup (rev); + prev_rev = xstrdup(branchname); + free (branchname); + } + + } else /* Not a branch */ + { + + /* Get current head revision of file. */ + if (rcsnode == NULL) + { + error (0, 0, "could not find parsed rcsfile %s", file); + return (1); + } + prev_rev = RCS_head (rcsnode); + } + + /* if removing without a tag or a branch, then make sure the default + branch is the trunk. */ + if (!tag && !branch) + { + if (RCS_setbranch (rcs, NULL) != 0) + { + error (0, 0, "cannot change branch to default for %s", + rcs); + return (1); + } + } + +#ifdef SERVER_SUPPORT + if (server_active) { + /* If this is the server, there will be a file sitting in the + temp directory which is the kludgy way in which server.c + tells time_stamp that the file is no longer around. Remove + it so we can create temp files with that name (ignore errors). */ + unlink_file (file); + } +#endif + + /* check something out. Generally this is the head. If we have a + particular rev, then name it. except when creating a branch, + lock the rev we're checking out. */ + retcode = RCS_checkout (rcs, "", rev ? corev : NULL, NULL, RUN_TTY, + lockflag, 1); + if (retcode != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to check out `%s'", rcs); + return (1); + } + + if (corev != NULL) + free (corev); + + retcode = RCS_checkin (rcs, NULL, message, rev, RCS_FLAGS_DEAD, 1); + if (retcode != 0) + { + if (!quiet) + error (0, retcode == -1 ? errno : 0, + "failed to commit dead revision for `%s'", rcs); + return (1); + } + + if (rev != NULL) + free (rev); + + if (!branch) + { + /* this was the head; really move it into the Attic */ + tmp = xmalloc(strlen(repository) + + sizeof('/') + + sizeof(CVSATTIC) + + sizeof('/') + + strlen(file) + + sizeof(RCSEXT) + 1); + (void) sprintf (tmp, "%s/%s", repository, CVSATTIC); + omask = umask (cvsumask); + (void) CVS_MKDIR (tmp, 0777); + (void) umask (omask); + (void) sprintf (tmp, "%s/%s/%s%s", repository, CVSATTIC, file, RCSEXT); + + if (strcmp (rcs, tmp) != 0 + && rename (rcs, tmp) == -1 + && (isreadable (rcs) || !isreadable (tmp))) + { + free(tmp); + return (1); + } + free(tmp); + } + + /* Print message that file was removed. */ + (void) printf ("%s <-- %s\n", rcs, file); + (void) printf ("new revision: delete; "); + (void) printf ("previous revision: %s\n", prev_rev); + (void) printf ("done\n"); + free(prev_rev); + + Scratch_Entry (entries, file); + return (0); +} + +/* + * Do the actual checkin for added files + */ +static int +finaladd (file, rev, tag, options, update_dir, repository, entries) + char *file; + char *rev; + char *tag; + char *options; + char *update_dir; + char *repository; + List *entries; +{ + int ret; + char tmp[PATH_MAX]; + char rcs[PATH_MAX]; + + locate_rcs (file, repository, rcs); + ret = Checkin ('A', file, update_dir, repository, rcs, rev, tag, options, + message, entries); + if (ret == 0) + { + (void) sprintf (tmp, "%s/%s%s", CVSADM, file, CVSEXT_LOG); + (void) unlink_file (tmp); + } + else + fixaddfile (file, repository); + + (void) time (&last_register_time); + + return (ret); +} + +/* + * Unlock an rcs file + */ +static void +unlockrcs (file, repository) + char *file; + char *repository; +{ + char rcs[PATH_MAX]; + int retcode = 0; + + locate_rcs (file, repository, rcs); + + if ((retcode = RCS_unlock (rcs, NULL, 0)) != 0) + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not unlock %s", rcs); +} + +/* + * remove a partially added file. if we can parse it, leave it alone. + */ +static void +fixaddfile (file, repository) + char *file; + char *repository; +{ + RCSNode *rcsfile; + char rcs[PATH_MAX]; + int save_really_quiet; + + locate_rcs (file, repository, rcs); + save_really_quiet = really_quiet; + really_quiet = 1; + if ((rcsfile = RCS_parsercsfile (rcs)) == NULL) + (void) unlink_file (rcs); + else + freercsnode (&rcsfile); + really_quiet = save_really_quiet; +} + +/* + * put the branch back on an rcs file + */ +static void +fixbranch (file, repository, branch) + char *file; + char *repository; + char *branch; +{ + char rcs[PATH_MAX]; + int retcode = 0; + + if (branch != NULL && branch[0] != '\0') + { + locate_rcs (file, repository, rcs); + 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); + } +} + +/* + * 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. + */ + +static int +checkaddfile (file, repository, tag, options, rcsnode) + char *file; + char *repository; + char *tag; + char *options; + RCSNode **rcsnode; +{ + char rcs[PATH_MAX]; + char fname[PATH_MAX]; + mode_t omask; + int retcode = 0; + int newfile = 0; + + if (tag) + { + (void) sprintf(rcs, "%s/%s", repository, CVSATTIC); + omask = umask (cvsumask); + if (CVS_MKDIR (rcs, 0777) != 0 && errno != EEXIST) + error (1, errno, "cannot make directory `%s'", rcs);; + (void) umask (omask); + (void) sprintf (rcs, "%s/%s/%s%s", repository, CVSATTIC, file, RCSEXT); + } + else + locate_rcs (file, repository, rcs); + + if (isreadable(rcs)) + { + /* file has existed in the past. Prepare to resurrect. */ + char oldfile[PATH_MAX]; + char *rev; + RCSNode *rcsfile; + + if (tag == NULL) + { + /* we are adding on the trunk, so move the file out of the + Attic. */ + strcpy (oldfile, rcs); + sprintf (rcs, "%s/%s%s", repository, file, RCSEXT); + + if (strcmp (oldfile, rcs) == 0 + || rename (oldfile, rcs) != 0 + || isreadable (oldfile) + || !isreadable (rcs)) + { + error (0, 0, "failed to move `%s' out of the attic.", + file); + return (1); + } + } + + if ((rcsfile = *rcsnode) == NULL) + { + error (0, 0, "could not find parsed rcsfile %s", file); + return (1); + } + + rev = RCS_getversion (rcsfile, tag, NULL, 1, 0); + /* and lock it */ + if (lock_RCS (file, rcs, rev, repository)) { + error (0, 0, "cannot lock `%s'.", rcs); + free (rev); + return (1); + } + + free (rev); + } else { + /* this is the first time we have ever seen this file; create + an rcs file. */ + run_setup ("%s%s -x,v/ -i", Rcsbin, RCS); + + (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)) + run_args ("-t%s/%s%s", CVSADM, file, CVSEXT_LOG); + + /* Set RCS keyword expansion options. */ + if (options && options[0] == '-' && options[1] == 'k') + run_arg (options); + run_arg (rcs); + if ((retcode = run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL)) != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not create %s", rcs); + return (1); + } + newfile = 1; + } + + /* when adding a file for the first time, and using a tag, we need + to create a dead revision on the trunk. */ + if (tag && newfile) + { + char *tmp; + + /* move the new file out of the way. */ + (void) sprintf (fname, "%s/%s%s", CVSADM, CVSPREFIX, file); + rename_file (file, fname); + copy_file (DEVNULL, 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, + RCS_FLAGS_DEAD | RCS_FLAGS_QUIET, 0); + free (tmp); + if (retcode != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not create initial dead revision %s", rcs); + return (1); + } + + /* put the new file back where it was */ + rename_file (fname, file); + + /* and lock it once again. */ + if (lock_RCS (file, rcs, NULL, repository)) { + error (0, 0, "cannot lock `%s'.", rcs); + return (1); + } + } + + if (tag != NULL) + { + /* when adding with a tag, we need to stub a branch, if it + doesn't already exist. */ + RCSNode *rcsfile; + + rcsfile = RCS_parse (file, repository); + if (rcsfile == NULL) + { + error (0, 0, "could not read %s", rcs); + return (1); + } + + if (!RCS_nodeisbranch (rcsfile, tag)) { + /* branch does not exist. Stub it. */ + char *head; + char *magicrev; + + head = RCS_getversion (rcsfile, NULL, NULL, 0, 0); + magicrev = RCS_magicrev (rcsfile, head); + if ((retcode = RCS_settag(rcs, tag, magicrev)) != 0) + { + error (retcode == -1 ? 1 : 0, retcode == -1 ? errno : 0, + "could not stub branch %s for %s", tag, rcs); + return (1); + } + + freercsnode (&rcsfile); + + /* reparse the file, then add it to our list. */ + rcsfile = RCS_parse (file, repository); + if (rcsfile == NULL) + { + error (0, 0, "could not reparse %s", rcs); + return (1); + } + + free (head); + free (magicrev); + } + else + { + /* lock the branch. (stubbed branches need not be locked.) */ + if (lock_RCS (file, rcs, NULL, repository)) { + error (0, 0, "cannot lock `%s'.", rcs); + return (1); + } + } + + if (rcsnode) + freercsnode(rcsnode); + *rcsnode = rcsfile; + } + + fileattr_newfile (file); + + fix_rcs_modes (rcs, file); + return (0); +} + +/* + * Lock the rcs file ``file'' + */ +static int +lockrcsfile (file, repository, rev) + char *file; + char *repository; + char *rev; +{ + char rcs[PATH_MAX]; + + locate_rcs (file, repository, rcs); + if (lock_RCS (file, rcs, rev, repository) != 0) + return (1); + else + return (0); +} + +/* + * 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) + char *user; + char *rcs; + char *rev; + char *repository; +{ + RCSNode *rcsfile; + 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 (*rev) && numdots (rev) < 2)) + { + if ((rcsfile = RCS_parsercsfile (rcs)) == NULL) + { + /* invalid rcs file? */ + err = 1; + } + else + { + /* rcsfile is valid */ + branch = xstrdup (rcsfile->branch); + freercsnode (&rcsfile); + if (branch != NULL) + { + if (RCS_setbranch (rcs, NULL) != 0) + { + error (0, 0, "cannot change branch to default for %s", + rcs); + if (branch) + free (branch); + return (1); + } + } + err = RCS_lock(rcs, NULL, 0); + } + } + else + { + (void) RCS_lock(rcs, rev, 1); + } + + if (err == 0) + { + if (branch) + { + (void) strcpy (sbranch, branch); + free (branch); + } + else + sbranch[0] = '\0'; + return (0); + } + + /* try to restore the branch if we can on error */ + if (branch != NULL) + fixbranch (user, repository, branch); + + if (branch) + free (branch); + return (1); +} + +/* + * Called when "add"ing files to the RCS respository, as it is necessary to + * preserve the file modes in the same fashion that RCS does. This would be + * automatic except that we are placing the RCS ,v file very far away from + * the user file, and I can't seem to convince RCS of the location of the + * user file. So we munge it here, after the ,v file has been successfully + * initialized with "rcs -i". + */ +static void +fix_rcs_modes (rcs, user) + char *rcs; + char *user; +{ + struct stat sb; + + if (stat (user, &sb) != -1) + (void) chmod (rcs, (int) sb.st_mode & ~0222); +} + +/* + * free an UPDATE node's data (really nothing to do) + */ +void +update_delproc (p) + Node *p; +{ + p->data = (char *) NULL; +} + +/* + * Free the commit_info structure in p. + */ +static void +ci_delproc (p) + Node *p; +{ + struct commit_info *ci; + + ci = (struct commit_info *) 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; + + ml = (struct master_lists *) p->data; + dellist (&ml->ulist); + dellist (&ml->cilist); + free (ml); +} + +/* + * Find an RCS file in the repository. + */ +static void +locate_rcs (file, repository, rcs) + char *file; + char *repository; + char *rcs; +{ + (void) sprintf (rcs, "%s/%s%s", repository, file, RCSEXT); + if (!isreadable (rcs)) + { + (void) sprintf (rcs, "%s/%s/%s%s", repository, CVSATTIC, file, RCSEXT); + if (!isreadable (rcs)) + (void) sprintf (rcs, "%s/%s%s", repository, file, RCSEXT); + } +} |