/* * Copyright (C) 1986-2005 The Free Software Foundation, Inc. * * Portions Copyright (C) 1998-2005 Derek Price, Ximbiot , * 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. * * Tag and Rtag * * Add or delete a symbolic name to an RCS file, or a collection of RCS files. * Tag uses the checked out revision in the current directory, rtag uses * the modules database, if necessary. */ #include "cvs.h" #include "save-cwd.h" static int rtag_proc (int argc, char **argv, char *xwhere, char *mwhere, char *mfile, int shorten, int local_specified, char *mname, char *msg); static int check_fileproc (void *callerdat, struct file_info *finfo); static int check_filesdoneproc (void *callerdat, int err, const char *repos, const char *update_dir, List *entries); static int pretag_proc (const char *_repository, const char *_filter, void *_closure); static void masterlist_delproc (Node *_p); static void tag_delproc (Node *_p); static int pretag_list_to_args_proc (Node *_p, void *_closure); static Dtype tag_dirproc (void *callerdat, const char *dir, const char *repos, const char *update_dir, List *entries); static int rtag_fileproc (void *callerdat, struct file_info *finfo); static int rtag_delete (RCSNode *rcsfile); static int tag_fileproc (void *callerdat, struct file_info *finfo); static char *numtag; /* specific revision to tag */ static bool numtag_validated = false; static char *date = NULL; static char *symtag; /* tag to add or delete */ static bool delete_flag; /* adding a tag by default */ static bool branch_mode; /* make an automagic "branch" tag */ static bool disturb_branch_tags = false;/* allow -F,-d to disturb branch tags */ static bool force_tag_match = true; /* force tag to match by default */ static bool force_tag_move; /* don't force tag to move by default */ static bool check_uptodate; /* no uptodate-check by default */ static bool attic_too; /* remove tag from Attic files */ static bool is_rtag; struct tag_info { Ctype status; char *oldrev; char *rev; char *tag; char *options; }; struct master_lists { List *tlist; }; static List *mtlist; static const char rtag_opts[] = "+aBbdFflnQqRr:D:"; static const char *const rtag_usage[] = { "Usage: %s %s [-abdFflnR] [-r rev|-D date] tag modules...\n", "\t-a\tClear tag from removed files that would not otherwise be tagged.\n", "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n", "\t-B\tAllows -F and -d to disturb branch tags. Use with extreme care.\n", "\t-d\tDelete the given tag.\n", "\t-F\tMove tag if it already exists.\n", "\t-f\tForce a head revision match if tag/date not found.\n", "\t-l\tLocal directory only, not recursive.\n", "\t-n\tNo execution of 'tag program'.\n", "\t-R\tProcess directories recursively.\n", "\t-r rev\tExisting revision/tag.\n", "\t-D\tExisting date.\n", "(Specify the --help global option for a list of other help options)\n", NULL }; static const char tag_opts[] = "+BbcdFflQqRr:D:"; static const char *const tag_usage[] = { "Usage: %s %s [-bcdFflR] [-r rev|-D date] tag [files...]\n", "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n", "\t-B\tAllows -F and -d to disturb branch tags. Use with extreme care.\n", "\t-c\tCheck that working files are unmodified.\n", "\t-d\tDelete the given tag.\n", "\t-F\tMove tag if it already exists.\n", "\t-f\tForce a head revision match if tag/date not found.\n", "\t-l\tLocal directory only, not recursive.\n", "\t-R\tProcess directories recursively.\n", "\t-r rev\tExisting revision/tag.\n", "\t-D\tExisting date.\n", "(Specify the --help global option for a list of other help options)\n", NULL }; int cvstag (int argc, char **argv) { bool local = false; /* recursive by default */ int c; int err = 0; bool run_module_prog = true; is_rtag = (strcmp (cvs_cmd_name, "rtag") == 0); if (argc == -1) usage (is_rtag ? rtag_usage : tag_usage); optind = 0; while ((c = getopt (argc, argv, is_rtag ? rtag_opts : tag_opts)) != -1) { switch (c) { case 'a': attic_too = true; break; case 'b': branch_mode = true; break; case 'B': disturb_branch_tags = true; break; case 'c': check_uptodate = true; break; case 'd': delete_flag = true; break; case 'F': force_tag_move = true; break; case 'f': force_tag_match = false; break; case 'l': local = true; break; case 'n': run_module_prog = false; break; case 'Q': case 'q': /* The CVS 1.5 client sends these options (in addition to Global_option requests), so we must ignore them. */ if (!server_active) error (1, 0, "-q or -Q must be specified before \"%s\"", cvs_cmd_name); break; case 'R': local = false; break; case 'r': parse_tagdate (&numtag, &date, optarg); break; case 'D': if (date) free (date); date = Make_Date (optarg); break; case '?': default: usage (is_rtag ? rtag_usage : tag_usage); break; } } argc -= optind; argv += optind; if (argc < (is_rtag ? 2 : 1)) usage (is_rtag ? rtag_usage : tag_usage); symtag = argv[0]; argc--; argv++; if (date && delete_flag) error (1, 0, "-d makes no sense with a date specification."); if (delete_flag && branch_mode) error (0, 0, "warning: -b ignored with -d options"); RCS_check_tag (symtag); #ifdef CLIENT_SUPPORT if (current_parsed_root->isremote) { /* We're the client side. Fire up the remote server. */ start_server (); ign_setup (); if (attic_too) send_arg ("-a"); if (branch_mode) send_arg ("-b"); if (disturb_branch_tags) send_arg ("-B"); if (check_uptodate) send_arg ("-c"); if (delete_flag) send_arg ("-d"); if (force_tag_move) send_arg ("-F"); if (!force_tag_match) send_arg ("-f"); if (local) send_arg ("-l"); if (!run_module_prog) send_arg ("-n"); if (numtag) option_with_arg ("-r", numtag); if (date) client_senddate (date); send_arg ("--"); send_arg (symtag); if (is_rtag) { int i; for (i = 0; i < argc; ++i) send_arg (argv[i]); send_to_server ("rtag\012", 0); } else { send_files (argc, argv, local, 0, /* I think the -c case is like "cvs status", in which we really better be correct rather than being fast; it is just too confusing otherwise. */ check_uptodate ? 0 : SEND_NO_CONTENTS); send_file_names (argc, argv, SEND_EXPAND_WILD); send_to_server ("tag\012", 0); } return get_responses_and_close (); } #endif if (is_rtag) { DBM *db; int i; db = open_module (); for (i = 0; i < argc; i++) { /* XXX last arg should be repository, but doesn't make sense here */ history_write ('T', (delete_flag ? "D" : (numtag ? numtag : (date ? date : "A"))), symtag, argv[i], ""); err += do_module (db, argv[i], TAG, delete_flag ? "Untagging" : "Tagging", rtag_proc, NULL, 0, local, run_module_prog, 0, symtag); } close_module (db); } else { err = rtag_proc (argc + 1, argv - 1, NULL, NULL, NULL, 0, local, NULL, NULL); } return err; } struct pretag_proc_data { List *tlist; bool delete_flag; bool force_tag_move; char *symtag; }; /* * called from Parse_Info, this routine processes a line that came out * of the posttag file and turns it into a command and executes it. * * RETURNS * the absolute value of the return value of run_exec, which may or * may not be the return value of the child process. this is * contrained to return positive values because Parse_Info is summing * return values and testing for non-zeroness to signify one or more * of its callbacks having returned an error. */ static int posttag_proc (const char *repository, const char *filter, void *closure) { char *cmdline; const char *srepos = Short_Repository (repository); struct pretag_proc_data *ppd = closure; /* %t = tag being added/moved/removed * %o = operation = "add" | "mov" | "del" * %b = branch mode = "?" (delete ops - unknown) | "T" (branch) * | "N" (not branch) * %c = cvs_cmd_name * %p = path from $CVSROOT * %r = path from root * %{sVv} = attribute list = file name, old version tag will be deleted * from, new version tag will be added to (or * deleted from until * SUPPORT_OLD_INFO_FMT_STRINGS is undefined). */ /* * Cast any NULL arguments as appropriate pointers as this is an * stdarg function and we need to be certain the caller gets what * is expected. */ cmdline = format_cmdline ( #ifdef SUPPORT_OLD_INFO_FMT_STRINGS false, srepos, #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */ filter, "t", "s", ppd->symtag, "o", "s", ppd->delete_flag ? "del" : ppd->force_tag_move ? "mov" : "add", "b", "c", delete_flag ? '?' : branch_mode ? 'T' : 'N', "c", "s", cvs_cmd_name, #ifdef SERVER_SUPPORT "R", "s", referrer ? referrer->original : "NONE", #endif /* SERVER_SUPPORT */ "p", "s", srepos, "r", "s", current_parsed_root->directory, "sVv", ",", ppd->tlist, pretag_list_to_args_proc, (void *) NULL, (char *) NULL); if (!cmdline || !strlen (cmdline)) { if (cmdline) free (cmdline); error (0, 0, "pretag proc resolved to the empty string!"); return 1; } run_setup (cmdline); free (cmdline); return abs (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL)); } /* * Call any postadmin procs. */ static int tag_filesdoneproc (void *callerdat, int err, const char *repository, const char *update_dir, List *entries) { Node *p; List *mtlist, *tlist; struct pretag_proc_data ppd; TRACE (TRACE_FUNCTION, "tag_filesdoneproc (%d, %s, %s)", err, repository, update_dir); mtlist = callerdat; p = findnode (mtlist, update_dir); if (p != NULL) tlist = ((struct master_lists *) p->data)->tlist; else tlist = NULL; if (tlist == NULL || tlist->list->next == tlist->list) return err; ppd.tlist = tlist; ppd.delete_flag = delete_flag; ppd.force_tag_move = force_tag_move; ppd.symtag = symtag; Parse_Info (CVSROOTADM_POSTTAG, repository, posttag_proc, PIOPT_ALL, &ppd); return err; } /* * callback proc for doing the real work of tagging */ /* ARGSUSED */ static int rtag_proc (int argc, char **argv, char *xwhere, char *mwhere, char *mfile, int shorten, int local_specified, char *mname, char *msg) { /* Begin section which is identical to patch_proc--should this be abstracted out somehow? */ char *myargv[2]; int err = 0; int which; char *repository; char *where; #ifdef HAVE_PRINTF_PTR TRACE (TRACE_FUNCTION, "rtag_proc (argc=%d, argv=%p, xwhere=%s,\n" " mwhere=%s, mfile=%s, shorten=%d,\n" " local_specified=%d, mname=%s, msg=%s)", argc, (void *)argv, xwhere ? xwhere : "(null)", mwhere ? mwhere : "(null)", mfile ? mfile : "(null)", shorten, local_specified, mname ? mname : "(null)", msg ? msg : "(null)" ); #else TRACE (TRACE_FUNCTION, "rtag_proc (argc=%d, argv=%lx, xwhere=%s,\n" " mwhere=%s, mfile=%s, shorten=%d,\n" " local_specified=%d, mname=%s, msg=%s )", argc, (unsigned long)argv, xwhere ? xwhere : "(null)", mwhere ? mwhere : "(null)", mfile ? mfile : "(null)", shorten, local_specified, mname ? mname : "(null)", msg ? msg : "(null)" ); #endif if (is_rtag) { repository = xmalloc (strlen (current_parsed_root->directory) + strlen (argv[0]) + (mfile == NULL ? 0 : strlen (mfile) + 1) + 2); (void) sprintf (repository, "%s/%s", current_parsed_root->directory, argv[0]); where = xmalloc (strlen (argv[0]) + (mfile == NULL ? 0 : strlen (mfile) + 1) + 1); (void) strcpy (where, argv[0]); /* If MFILE isn't null, we need to set up to do only part of the * module. */ if (mfile != NULL) { char *cp; char *path; /* If the portion of the module is a path, put the dir part on * REPOS. */ if ((cp = strrchr (mfile, '/')) != NULL) { *cp = '\0'; (void) strcat (repository, "/"); (void) strcat (repository, mfile); (void) strcat (where, "/"); (void) strcat (where, mfile); mfile = cp + 1; } /* take care of the rest */ path = xmalloc (strlen (repository) + strlen (mfile) + 5); (void) sprintf (path, "%s/%s", repository, mfile); if (isdir (path)) { /* directory means repository gets the dir tacked on */ (void) strcpy (repository, path); (void) strcat (where, "/"); (void) strcat (where, mfile); } else { myargv[0] = argv[0]; myargv[1] = mfile; argc = 2; argv = myargv; } free (path); } /* cd to the starting repository */ if (CVS_CHDIR (repository) < 0) { error (0, errno, "cannot chdir to %s", repository); free (repository); free (where); return 1; } /* End section which is identical to patch_proc. */ if (delete_flag || attic_too || (force_tag_match && numtag)) which = W_REPOS | W_ATTIC; else which = W_REPOS; } else { where = NULL; which = W_LOCAL; repository = ""; } if (numtag != NULL && !numtag_validated) { tag_check_valid (numtag, argc - 1, argv + 1, local_specified, 0, repository, false); numtag_validated = true; } /* check to make sure they are authorized to tag all the specified files in the repository */ mtlist = getlist (); err = start_recursion (check_fileproc, check_filesdoneproc, NULL, NULL, NULL, argc - 1, argv + 1, local_specified, which, 0, CVS_LOCK_READ, where, 1, repository); if (err) { error (1, 0, "correct the above errors first!"); } /* It would be nice to provide consistency with respect to commits; however CVS lacks the infrastructure to do that (see Concurrency in cvs.texinfo and comment in do_recursion). */ /* start the recursion processor */ err = start_recursion (is_rtag ? rtag_fileproc : tag_fileproc, tag_filesdoneproc, tag_dirproc, NULL, mtlist, argc - 1, argv + 1, local_specified, which, 0, CVS_LOCK_WRITE, where, 1, repository); dellist (&mtlist); if (which & W_REPOS) free (repository); if (where != NULL) free (where); return err; } /* check file that is to be tagged */ /* All we do here is add it to our list */ static int check_fileproc (void *callerdat, struct file_info *finfo) { const char *xdir; Node *p; Vers_TS *vers; List *tlist; struct tag_info *ti; int addit = 1; TRACE (TRACE_FUNCTION, "check_fileproc (%s, %s, %s)", finfo->repository ? finfo->repository : "(null)", finfo->fullname ? finfo->fullname : "(null)", finfo->rcs ? (finfo->rcs->path ? finfo->rcs->path : "(null)") : "NULL"); if (check_uptodate) { switch (Classify_File (finfo, NULL, NULL, NULL, 1, 0, &vers, 0)) { case T_UPTODATE: case T_CHECKOUT: case T_PATCH: case T_REMOVE_ENTRY: break; case T_UNKNOWN: case T_CONFLICT: case T_NEEDS_MERGE: case T_MODIFIED: case T_ADDED: case T_REMOVED: default: error (0, 0, "%s is locally modified", finfo->fullname); freevers_ts (&vers); return 1; } } else vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); if (finfo->update_dir[0] == '\0') xdir = "."; else xdir = finfo->update_dir; if ((p = findnode (mtlist, xdir)) != NULL) { tlist = ((struct master_lists *) p->data)->tlist; } else { struct master_lists *ml; tlist = getlist (); p = getnode (); p->key = xstrdup (xdir); p->type = UPDATE; ml = xmalloc (sizeof (struct master_lists)); ml->tlist = tlist; p->data = ml; p->delproc = masterlist_delproc; (void) addnode (mtlist, p); } /* do tlist */ p = getnode (); p->key = xstrdup (finfo->file); p->type = UPDATE; p->delproc = tag_delproc; if (vers->srcfile == NULL) { if (!really_quiet) error (0, 0, "nothing known about %s", finfo->file); freevers_ts (&vers); freenode (p); return 1; } /* Here we duplicate the calculation in tag_fileproc about which version we are going to tag. There probably are some subtle races (e.g. numtag is "foo" which gets moved between here and tag_fileproc). */ p->data = ti = xmalloc (sizeof (struct tag_info)); ti->tag = xstrdup (numtag ? numtag : vers->tag); if (!is_rtag && numtag == NULL && date == NULL) ti->rev = xstrdup (vers->vn_user); else ti->rev = RCS_getversion (vers->srcfile, numtag, date, force_tag_match, NULL); if (ti->rev != NULL) { ti->oldrev = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL); if (ti->oldrev == NULL) { if (delete_flag) { /* Deleting a tag which did not exist is a noop and should not be logged. */ addit = 0; } } else if (delete_flag) { free (ti->rev); #ifdef SUPPORT_OLD_INFO_FMT_STRINGS /* a hack since %v used to mean old or new rev */ ti->rev = xstrdup (ti->oldrev); #else /* SUPPORT_OLD_INFO_FMT_STRINGS */ ti->rev = NULL; #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */ } else if (strcmp(ti->oldrev, p->data) == 0) addit = 0; else if (!force_tag_move) addit = 0; } else addit = 0; if (!addit) { free(p->data); p->data = NULL; } freevers_ts (&vers); (void)addnode (tlist, p); return 0; } static int check_filesdoneproc (void *callerdat, int err, const char *repos, const char *update_dir, List *entries) { int n; Node *p; List *tlist; struct pretag_proc_data ppd; p = findnode (mtlist, update_dir); if (p != NULL) tlist = ((struct master_lists *) p->data)->tlist; else tlist = NULL; if (tlist == NULL || tlist->list->next == tlist->list) return err; ppd.tlist = tlist; ppd.delete_flag = delete_flag; ppd.force_tag_move = force_tag_move; ppd.symtag = symtag; if ((n = Parse_Info (CVSROOTADM_TAGINFO, repos, pretag_proc, PIOPT_ALL, &ppd)) > 0) { error (0, 0, "Pre-tag check failed"); err += n; } return err; } /* * called from Parse_Info, this routine processes a line that came out * of a taginfo file and turns it into a command and executes it. * * RETURNS * the absolute value of the return value of run_exec, which may or * may not be the return value of the child process. this is * contrained to return positive values because Parse_Info is adding up * return values and testing for non-zeroness to signify one or more * of its callbacks having returned an error. */ static int pretag_proc (const char *repository, const char *filter, void *closure) { char *newfilter = NULL; char *cmdline; const char *srepos = Short_Repository (repository); struct pretag_proc_data *ppd = closure; #ifdef SUPPORT_OLD_INFO_FMT_STRINGS if (!strchr (filter, '%')) { error (0,0, "warning: taginfo line contains no format strings:\n" " \"%s\"\n" "Filling in old defaults ('%%t %%o %%p %%{sv}'), but please be aware that this\n" "usage is deprecated.", filter); newfilter = xmalloc (strlen (filter) + 16); strcpy (newfilter, filter); strcat (newfilter, " %t %o %p %{sv}"); filter = newfilter; } #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */ /* %t = tag being added/moved/removed * %o = operation = "add" | "mov" | "del" * %b = branch mode = "?" (delete ops - unknown) | "T" (branch) * | "N" (not branch) * %c = cvs_cmd_name * %p = path from $CVSROOT * %r = path from root * %{sVv} = attribute list = file name, old version tag will be deleted * from, new version tag will be added to (or * deleted from until * SUPPORT_OLD_INFO_FMT_STRINGS is undefined) */ /* * Cast any NULL arguments as appropriate pointers as this is an * stdarg function and we need to be certain the caller gets what * is expected. */ cmdline = format_cmdline ( #ifdef SUPPORT_OLD_INFO_FMT_STRINGS false, srepos, #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */ filter, "t", "s", ppd->symtag, "o", "s", ppd->delete_flag ? "del" : ppd->force_tag_move ? "mov" : "add", "b", "c", delete_flag ? '?' : branch_mode ? 'T' : 'N', "c", "s", cvs_cmd_name, #ifdef SERVER_SUPPORT "R", "s", referrer ? referrer->original : "NONE", #endif /* SERVER_SUPPORT */ "p", "s", srepos, "r", "s", current_parsed_root->directory, "sVv", ",", ppd->tlist, pretag_list_to_args_proc, (void *) NULL, (char *) NULL); if (newfilter) free (newfilter); if (!cmdline || !strlen (cmdline)) { if (cmdline) free (cmdline); error (0, 0, "pretag proc resolved to the empty string!"); return 1; } run_setup (cmdline); /* FIXME - the old code used to run the following here: * * if (!isfile(s)) * { * error (0, errno, "cannot find pre-tag filter '%s'", s); * free(s); * return (1); * } * * not sure this is really necessary. it might give a little finer grained * error than letting the execution attempt fail but i'm not sure. in any * case it should be easy enough to add a function in run.c to test its * first arg for fileness & executability. */ free (cmdline); return abs (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL)); } static void masterlist_delproc (Node *p) { struct master_lists *ml = p->data; dellist (&ml->tlist); free (ml); return; } static void tag_delproc (Node *p) { struct tag_info *ti; if (p->data) { ti = (struct tag_info *) p->data; if (ti->oldrev) free (ti->oldrev); if (ti->rev) free (ti->rev); free (ti->tag); free (p->data); p->data = NULL; } return; } /* to be passed into walklist with a list of tags * p->key = tagname * p->data = struct tag_info * * p->data->oldrev = rev tag will be deleted from * p->data->rev = rev tag will be added to * p->data->tag = tag oldrev is attached to, if any * * closure will be a struct format_cmdline_walklist_closure * where closure is undefined */ static int pretag_list_to_args_proc (Node *p, void *closure) { struct tag_info *taginfo = (struct tag_info *)p->data; struct format_cmdline_walklist_closure *c = (struct format_cmdline_walklist_closure *)closure; char *arg = NULL; const char *f; char *d; size_t doff; if (!p->data) return 1; f = c->format; d = *c->d; /* foreach requested attribute */ while (*f) { switch (*f++) { case 's': arg = p->key; break; case 'T': arg = taginfo->tag ? taginfo->tag : ""; break; case 'v': arg = taginfo->rev ? taginfo->rev : "NONE"; break; case 'V': arg = taginfo->oldrev ? taginfo->oldrev : "NONE"; break; default: error(1,0, "Unknown format character or not a list attribute: %c", f[-1]); break; } /* copy the attribute into an argument */ if (c->quotes) { arg = cmdlineescape (c->quotes, arg); } else { arg = cmdlinequote ('"', arg); } doff = d - *c->buf; expand_string (c->buf, c->length, doff + strlen (arg)); d = *c->buf + doff; strncpy (d, arg, strlen (arg)); d += strlen (arg); free (arg); /* and always put the extra space on. we'll have to back up a char when we're * done, but that seems most efficient */ doff = d - *c->buf; expand_string (c->buf, c->length, doff + 1); d = *c->buf + doff; *d++ = ' '; } /* correct our original pointer into the buff */ *c->d = d; return 0; } /* * Called to rtag a particular file, as appropriate with the options that were * set above. */ /* ARGSUSED */ static int rtag_fileproc (void *callerdat, struct file_info *finfo) { RCSNode *rcsfile; char *version = NULL, *rev = NULL; int retcode = 0; int retval = 0; static bool valtagged = false; /* find the parsed RCS data */ if ((rcsfile = finfo->rcs) == NULL) { retval = 1; goto free_vars_and_return; } /* * For tagging an RCS file which is a symbolic link, you'd best be * running with RCS 5.6, since it knows how to handle symbolic links * correctly without breaking your link! */ if (delete_flag) { retval = rtag_delete (rcsfile); goto free_vars_and_return; } /* * If we get here, we are adding a tag. But, if -a was specified, we * need to check to see if a -r or -D option was specified. If neither * was specified and the file is in the Attic, remove the tag. */ if (attic_too && (!numtag && !date)) { if ((rcsfile->flags & VALID) && (rcsfile->flags & INATTIC)) { retval = rtag_delete (rcsfile); goto free_vars_and_return; } } version = RCS_getversion (rcsfile, numtag, date, force_tag_match, NULL); if (version == NULL) { /* If -a specified, clean up any old tags */ if (attic_too) (void)rtag_delete (rcsfile); if (!quiet && !force_tag_match) { error (0, 0, "cannot find tag `%s' in `%s'", numtag ? numtag : "head", rcsfile->path); retval = 1; } goto free_vars_and_return; } if (numtag && isdigit ((unsigned char)*numtag) && strcmp (numtag, version) != 0) { /* * We didn't find a match for the numeric tag that was specified, but * that's OK. just pass the numeric tag on to rcs, to be tagged as * specified. Could get here if one tried to tag "1.1.1" and there * was a 1.1.1 branch with some head revision. In this case, we want * the tag to reference "1.1.1" and not the revision at the head of * the branch. Use a symbolic tag for that. */ rev = branch_mode ? RCS_magicrev (rcsfile, version) : numtag; retcode = RCS_settag(rcsfile, symtag, numtag); if (retcode == 0) RCS_rewrite (rcsfile, NULL, NULL); } else { char *oversion; /* * As an enhancement for the case where a tag is being re-applied to * a large body of a module, make one extra call to RCS_getversion to * see if the tag is already set in the RCS file. If so, check to * see if it needs to be moved. If not, do nothing. This will * likely save a lot of time when simply moving the tag to the * "current" head revisions of a module -- which I have found to be a * typical tagging operation. */ rev = branch_mode ? RCS_magicrev (rcsfile, version) : version; oversion = RCS_getversion (rcsfile, symtag, NULL, 1, NULL); if (oversion != NULL) { int isbranch = RCS_nodeisbranch (finfo->rcs, symtag); /* * if versions the same and neither old or new are branches don't * have to do anything */ if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch) { free (oversion); goto free_vars_and_return; } if (!force_tag_move) { /* we're NOT going to move the tag */ (void)printf ("W %s", finfo->fullname); (void)printf (" : %s already exists on %s %s", symtag, isbranch ? "branch" : "version", oversion); (void)printf (" : NOT MOVING tag to %s %s\n", branch_mode ? "branch" : "version", rev); free (oversion); goto free_vars_and_return; } else /* force_tag_move is set and... */ if ((isbranch && !disturb_branch_tags) || (!isbranch && disturb_branch_tags)) { error(0,0, "%s: Not moving %s tag `%s' from %s to %s%s.", finfo->fullname, isbranch ? "branch" : "non-branch", symtag, oversion, rev, isbranch ? "" : " due to `-B' option"); free (oversion); goto free_vars_and_return; } free (oversion); } retcode = RCS_settag (rcsfile, symtag, rev); if (retcode == 0) RCS_rewrite (rcsfile, NULL, NULL); } if (retcode != 0) { error (1, retcode == -1 ? errno : 0, "failed to set tag `%s' to revision `%s' in `%s'", symtag, rev, rcsfile->path); retval = 1; goto free_vars_and_return; } free_vars_and_return: if (branch_mode && rev) free (rev); if (version) free (version); if (!delete_flag && !retval && !valtagged) { tag_check_valid (symtag, 0, NULL, 0, 0, NULL, true); valtagged = true; } return retval; } /* * If -d is specified, "force_tag_match" is set, so that this call to * RCS_getversion() will return a NULL version string if the symbolic * tag does not exist in the RCS file. * * If the -r flag was used, numtag is set, and we only delete the * symtag from files that have numtag. * * This is done here because it's MUCH faster than just blindly calling * "rcs" to remove the tag... trust me. */ static int rtag_delete (RCSNode *rcsfile) { char *version; int retcode, isbranch; if (numtag) { version = RCS_getversion (rcsfile, numtag, NULL, 1, NULL); if (version == NULL) return (0); free (version); } version = RCS_getversion (rcsfile, symtag, NULL, 1, NULL); if (version == NULL) return 0; free (version); isbranch = RCS_nodeisbranch (rcsfile, symtag); if ((isbranch && !disturb_branch_tags) || (!isbranch && disturb_branch_tags)) { if (!quiet) error (0, 0, "Not removing %s tag `%s' from `%s'%s.", isbranch ? "branch" : "non-branch", symtag, rcsfile->path, isbranch ? "" : " due to `-B' option"); return 1; } if ((retcode = RCS_deltag(rcsfile, symtag)) != 0) { if (!quiet) error (0, retcode == -1 ? errno : 0, "failed to remove tag `%s' from `%s'", symtag, rcsfile->path); return 1; } RCS_rewrite (rcsfile, NULL, NULL); return 0; } /* * Called to tag a particular file (the currently checked out version is * tagged with the specified tag - or the specified tag is deleted). */ /* ARGSUSED */ static int tag_fileproc (void *callerdat, struct file_info *finfo) { char *version, *oversion; char *nversion = NULL; char *rev; Vers_TS *vers; int retcode = 0; int retval = 0; static bool valtagged = false; vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0); if (numtag || date) { nversion = RCS_getversion (vers->srcfile, numtag, date, force_tag_match, NULL); if (!nversion) goto free_vars_and_return; } if (delete_flag) { int isbranch; /* * If -d is specified, "force_tag_match" is set, so that this call to * RCS_getversion() will return a NULL version string if the symbolic * tag does not exist in the RCS file. * * This is done here because it's MUCH faster than just blindly calling * "rcs" to remove the tag... trust me. */ version = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL); if (version == NULL || vers->srcfile == NULL) goto free_vars_and_return; free (version); isbranch = RCS_nodeisbranch (finfo->rcs, symtag); if ((isbranch && !disturb_branch_tags) || (!isbranch && disturb_branch_tags)) { if (!quiet) error(0, 0, "Not removing %s tag `%s' from `%s'%s.", isbranch ? "branch" : "non-branch", symtag, vers->srcfile->path, isbranch ? "" : " due to `-B' option"); retval = 1; goto free_vars_and_return; } if ((retcode = RCS_deltag (vers->srcfile, symtag)) != 0) { if (!quiet) error (0, retcode == -1 ? errno : 0, "failed to remove tag %s from %s", symtag, vers->srcfile->path); retval = 1; goto free_vars_and_return; } RCS_rewrite (vers->srcfile, NULL, NULL); /* warm fuzzies */ if (!really_quiet) { cvs_output ("D ", 2); cvs_output (finfo->fullname, 0); cvs_output ("\n", 1); } goto free_vars_and_return; } /* * If we are adding a tag, we need to know which version we have checked * out and we'll tag that version. */ if (!nversion) version = vers->vn_user; else version = nversion; if (!version) goto free_vars_and_return; else if (strcmp (version, "0") == 0) { if (!quiet) error (0, 0, "couldn't tag added but un-commited file `%s'", finfo->file); goto free_vars_and_return; } else if (version[0] == '-') { if (!quiet) error (0, 0, "skipping removed but un-commited file `%s'", finfo->file); goto free_vars_and_return; } else if (vers->srcfile == NULL) { if (!quiet) error (0, 0, "cannot find revision control file for `%s'", finfo->file); goto free_vars_and_return; } /* * As an enhancement for the case where a tag is being re-applied to a * large number of files, make one extra call to RCS_getversion to see * if the tag is already set in the RCS file. If so, check to see if it * needs to be moved. If not, do nothing. This will likely save a lot of * time when simply moving the tag to the "current" head revisions of a * module -- which I have found to be a typical tagging operation. */ rev = branch_mode ? RCS_magicrev (vers->srcfile, version) : version; oversion = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL); if (oversion != NULL) { int isbranch = RCS_nodeisbranch (finfo->rcs, symtag); /* * if versions the same and neither old or new are branches don't have * to do anything */ if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch) { free (oversion); if (branch_mode) free (rev); goto free_vars_and_return; } if (!force_tag_move) { /* we're NOT going to move the tag */ cvs_output ("W ", 2); cvs_output (finfo->fullname, 0); cvs_output (" : ", 0); cvs_output (symtag, 0); cvs_output (" already exists on ", 0); cvs_output (isbranch ? "branch" : "version", 0); cvs_output (" ", 0); cvs_output (oversion, 0); cvs_output (" : NOT MOVING tag to ", 0); cvs_output (branch_mode ? "branch" : "version", 0); cvs_output (" ", 0); cvs_output (rev, 0); cvs_output ("\n", 1); free (oversion); if (branch_mode) free (rev); goto free_vars_and_return; } else /* force_tag_move == 1 and... */ if ((isbranch && !disturb_branch_tags) || (!isbranch && disturb_branch_tags)) { error (0,0, "%s: Not moving %s tag `%s' from %s to %s%s.", finfo->fullname, isbranch ? "branch" : "non-branch", symtag, oversion, rev, isbranch ? "" : " due to `-B' option"); free (oversion); if (branch_mode) free (rev); goto free_vars_and_return; } free (oversion); } if ((retcode = RCS_settag(vers->srcfile, symtag, rev)) != 0) { error (1, retcode == -1 ? errno : 0, "failed to set tag %s to revision %s in %s", symtag, rev, vers->srcfile->path); if (branch_mode) free (rev); retval = 1; goto free_vars_and_return; } if (branch_mode) free (rev); RCS_rewrite (vers->srcfile, NULL, NULL); /* more warm fuzzies */ if (!really_quiet) { cvs_output ("T ", 2); cvs_output (finfo->fullname, 0); cvs_output ("\n", 1); } free_vars_and_return: if (nversion != NULL) free (nversion); freevers_ts (&vers); if (!delete_flag && !retval && !valtagged) { tag_check_valid (symtag, 0, NULL, 0, 0, NULL, true); valtagged = true; } return retval; } /* * Print a warm fuzzy message */ /* ARGSUSED */ static Dtype tag_dirproc (void *callerdat, const char *dir, const char *repos, const char *update_dir, List *entries) { if (ignore_directory (update_dir)) { /* print the warm fuzzy message */ if (!quiet) error (0, 0, "Ignoring %s", update_dir); return R_SKIP_ALL; } if (!quiet) error (0, 0, "%s %s", delete_flag ? "Untagging" : "Tagging", update_dir); return R_PROCESS; } /* Code relating to the val-tags file. Note that this file has no way of knowing when a tag has been deleted. The problem is that there is no way of knowing whether a tag still exists somewhere, when we delete it some places. Using per-directory val-tags files (in CVSREP) might be better, but that might slow down the process of verifying that a tag is correct (maybe not, for the likely cases, if carefully done), and/or be harder to implement correctly. */ struct val_args { const char *name; int found; }; static int val_fileproc (void *callerdat, struct file_info *finfo) { RCSNode *rcsdata; struct val_args *args = callerdat; char *tag; if ((rcsdata = finfo->rcs) == NULL) /* Not sure this can happen, after all we passed only W_REPOS | W_ATTIC. */ return 0; tag = RCS_gettag (rcsdata, args->name, 1, NULL); if (tag != NULL) { /* FIXME: should find out a way to stop the search at this point. */ args->found = 1; free (tag); } return 0; } /* This routine determines whether a tag appears in CVSROOT/val-tags. * * The val-tags file will be open read-only when IDB is NULL. Since writes to * val-tags always append to it, the lack of locking is okay. The worst case * race condition might misinterpret a partially written "foobar" matched, for * instance, a request for "f", "foo", of "foob". Such a mismatch would be * caught harmlessly later. * * Before CVS adds a tag to val-tags, it will lock val-tags for write and * verify that the tag is still not present to avoid adding it twice. * * NOTES * This function expects its parent to handle any necessary locking of the * val-tags file. * * INPUTS * idb When this value is NULL, the val-tags file is opened in * in read-only mode. When present, the val-tags file is opened * in read-write mode and the DBM handle is stored in *IDB. * name The tag to search for. * * OUTPUTS * *idb The val-tags file opened for read/write, or NULL if it couldn't * be opened. * * ERRORS * Exits with an error message if the val-tags file cannot be opened for * read (failure to open val-tags read/write is harmless - see below). * * RETURNS * true 1. If NAME exists in val-tags. * 2. If IDB is non-NULL and val-tags cannot be opened for write. * This allows callers to ignore the harmless inability to * update the val-tags cache. * false If the file could be opened and the tag is not present. */ static int is_in_val_tags (DBM **idb, const char *name) { DBM *db = NULL; char *valtags_filename; datum mytag; int status; /* Casting out const should be safe here - input datums are not * written to by the myndbm functions. */ mytag.dptr = (char *)name; mytag.dsize = strlen (name); valtags_filename = Xasprintf ("%s/%s/%s", current_parsed_root->directory, CVSROOTADM, CVSROOTADM_VALTAGS); if (idb) { mode_t omask; omask = umask (cvsumask); db = dbm_open (valtags_filename, O_RDWR | O_CREAT, 0666); umask (omask); if (!db) { error (0, errno, "warning: cannot open `%s' read/write", valtags_filename); *idb = NULL; return 1; } *idb = db; } else { db = dbm_open (valtags_filename, O_RDONLY, 0444); if (!db && !existence_error (errno)) error (1, errno, "cannot read %s", valtags_filename); } /* If the file merely fails to exist, we just keep going and create it later if need be. */ status = 0; if (db) { datum val; val = dbm_fetch (db, mytag); if (val.dptr != NULL) /* Found. The tag is valid. */ status = 1; /* FIXME: should check errors somehow (add dbm_error to myndbm.c?). */ if (!idb) dbm_close (db); } free (valtags_filename); return status; } /* Add a tag to the CVSROOT/val-tags cache. Establishes a write lock and * reverifies that the tag does not exist before adding it. */ static void add_to_val_tags (const char *name) { DBM *db; datum mytag; datum value; if (noexec) return; val_tags_lock (current_parsed_root->directory); /* Check for presence again since we have a lock now. */ if (is_in_val_tags (&db, name)) return; /* Casting out const should be safe here - input datums are not * written to by the myndbm functions. */ mytag.dptr = (char *)name; mytag.dsize = strlen (name); value.dptr = "y"; value.dsize = 1; if (dbm_store (db, mytag, value, DBM_REPLACE) < 0) error (0, errno, "failed to store %s into val-tags", name); dbm_close (db); clear_val_tags_lock (); } static Dtype val_direntproc (void *callerdat, const char *dir, const char *repository, const char *update_dir, List *entries) { /* This is not quite right--it doesn't get right the case of "cvs update -d -r foobar" where foobar is a tag which exists only in files in a directory which does not exist yet, but which is about to be created. */ if (isdir (dir)) return R_PROCESS; return R_SKIP_ALL; } /* With VALID set, insert NAME into val-tags if it is not already present * there. * * Without VALID set, check to see whether NAME is a valid tag. If so, return. * If not print an error message and exit. * * INPUTS * * ARGC, ARGV, LOCAL, and AFLAG specify which files we will be operating on. * * REPOSITORY is the repository if we need to cd into it, or NULL if * we are already there, or "" if we should do a W_LOCAL recursion. * Sorry for three cases, but the "" case is needed in case the * working directories come from diverse parts of the repository, the * NULL case avoids an unneccesary chdir, and the non-NULL, non-"" * case is needed for checkout, where we don't want to chdir if the * tag is found in CVSROOTADM_VALTAGS, but there is not (yet) any * local directory. * * ERRORS * Errors may be encountered opening and accessing the DBM file. Write * errors generate warnings and read errors are fatal. When !VALID and NAME * is not in val-tags, errors may also be generated as per start_recursion. * When !VALID, non-existance of tags both in val-tags and in the archive * files also causes a fatal error. * * RETURNS * Nothing. */ void tag_check_valid (const char *name, int argc, char **argv, int local, int aflag, char *repository, bool valid) { struct val_args the_val_args; struct saved_cwd cwd; int which; #ifdef HAVE_PRINTF_PTR TRACE (TRACE_FUNCTION, "tag_check_valid (name=%s, argc=%d, argv=%p, local=%d,\n" " aflag=%d, repository=%s, valid=%s)", name ? name : "(name)", argc, (void *)argv, local, aflag, repository ? repository : "(null)", valid ? "true" : "false"); #else TRACE (TRACE_FUNCTION, "tag_check_valid (name=%s, argc=%d, argv=%lx, local=%d,\n" " aflag=%d, repository=%s, valid=%s)", name ? name : "(name)", argc, (unsigned long)argv, local, aflag, repository ? repository : "(null)", valid ? "true" : "false"); #endif /* Numeric tags require only a syntactic check. */ if (isdigit ((unsigned char) name[0])) { /* insert is not possible for numeric revisions */ assert (!valid); if (RCS_valid_rev (name)) return; else error (1, 0, "\ Numeric tag %s invalid. Numeric tags should be of the form X[.X]...", name); } /* Special tags are always valid. */ if (strcmp (name, TAG_BASE) == 0 || strcmp (name, TAG_HEAD) == 0) { /* insert is not possible for numeric revisions */ assert (!valid); return; } /* Verify that the tag is valid syntactically. Some later code once made * assumptions about this. */ RCS_check_tag (name); if (is_in_val_tags (NULL, name)) return; if (!valid) { /* We didn't find the tag in val-tags, so look through all the RCS files * to see whether it exists there. Yes, this is expensive, but there * is no other way to cope with a tag which might have been created * by an old version of CVS, from before val-tags was invented */ the_val_args.name = name; the_val_args.found = 0; which = W_REPOS | W_ATTIC; if (repository == NULL || repository[0] == '\0') which |= W_LOCAL; else { if (save_cwd (&cwd)) error (1, errno, "Failed to save current directory."); if (CVS_CHDIR (repository) < 0) error (1, errno, "cannot change to %s directory", repository); } start_recursion (val_fileproc, NULL, val_direntproc, NULL, &the_val_args, argc, argv, local, which, aflag, CVS_LOCK_READ, NULL, 1, repository); if (repository != NULL && repository[0] != '\0') { if (restore_cwd (&cwd)) error (1, errno, "Failed to restore current directory, `%s'.", cwd.name); free_cwd (&cwd); } if (!the_val_args.found) error (1, 0, "no such tag `%s'", name); } /* The tags is valid but not mentioned in val-tags. Add it. */ add_to_val_tags (name); }