/* * 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); } }