/* Implementation for "cvs edit", "cvs watch on", and related commands This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "cvs.h" #include "getline.h" #include "watch.h" #include "edit.h" #include "fileattr.h" static int check_edited = 0; static int watch_onoff(int argc, char **argv); static int setting_default; static int turning_on; static int keep_write; static int revert_only; static cvs::string bugid; static const char *message; static int exclusive_edit; static int whole_file; static int all_editors; static int verbose; static int check_edit; static int check_uptodate; #if defined(_WIN32) && !defined(CVS95) static int set_acl; #endif static int setting_tedit; static int setting_tunedit; static int setting_tcommit; static int editors_found; static int gzip_copies; static char *notify_user; static int force_unedit; /* Watcher who always gets notifications */ const char *global_watcher; static int unedit_fileproc (void *callerdat, struct file_info *finfo); static int onoff_fileproc(void *callerdat, struct file_info *finfo) { XmlHandle_t handle = fileattr_create(NULL,"file[@name=F'%s']",finfo->file); if(turning_on) fileattr_create(handle,"watched"); else { fileattr_delete(handle,"watched"); fileattr_prune(handle); } return 0; } static int onoff_filesdoneproc (void *callerdat, int err, char *repository, char *update_dir, List *entries) { if (setting_default) { XmlHandle_t handle = fileattr_create(NULL,"directory/default"); if(turning_on) fileattr_create(handle,"watched"); else fileattr_delete(handle,"watched"); } return err; } static int watch_onoff (int argc, char **argv) { int c; int local = 0; int err; optind = 0; while ((c = getopt (argc, argv, "+lR")) != -1) { switch (c) { case 'l': local = 1; break; case 'R': local = 0; break; case '?': default: usage (watch_usage); break; } } argc -= optind; argv += optind; if (current_parsed_root->isremote) { if (local) send_arg ("-l"); send_arg("--"); send_files (argc, argv, local, 0, SEND_NO_CONTENTS); send_file_names (argc, argv, SEND_EXPAND_WILD); send_to_server (turning_on ? "watch-on\n" : "watch-off\n", 0); return get_responses_and_close (); } setting_default = (argc <= 0); lock_tree_for_write (argc, argv, local, W_LOCAL, 0); err = start_recursion (onoff_fileproc, onoff_filesdoneproc, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, verify_write); Lock_Cleanup (); return err; } int watch_on (int argc, char **argv) { turning_on = 1; return watch_onoff (argc, argv); } int watch_off (int argc, char **argv) { turning_on = 0; return watch_onoff (argc, argv); } static int dummy_fileproc(void *callerdat, struct file_info *finfo) { /* This is a pretty hideous hack, but the gist of it is that recurse.c won't call notify_check unless there is a fileproc, so we can't just pass NULL for fileproc. */ return 0; } /* Check for and process notifications. Local only. I think that doing this as a fileproc is the only way to catch all the cases (e.g. foo/bar.c), even though that means checking over and over for the same CVSADM_NOTIFY file which we removed the first time we processed the directory. */ static int ncheck_fileproc (void *callerdat, struct file_info *finfo) { int notif_type; char *filename; char *cp,*cpo; char *watches; char *date, *hostname, *pathname, *flags, *bugno, *msg, *tag; int err = 0; List *rollback_list = NULL; FILE *fp; char *line = NULL; size_t line_len = 0; /* We send notifications even if noexec. I'm not sure which behavior is most sensible. */ fp = CVS_FOPEN (CVSADM_NOTIFY, "r"); if (fp == NULL) { if (!existence_error (errno)) error (0, errno, "cannot open %s", CVSADM_NOTIFY); return 0; } while (getline (&line, &line_len, fp) > 0) { notif_type = line[0]; if (notif_type == '\0') continue; filename = line + 1; cp = strchr (filename, '\t'); if (cp == NULL) continue; *cp++ = '\0'; date = cp; cp = strchr (date, '\t'); if (cp == NULL) continue; *cp++ = '\0'; hostname=cp; cp = strchr (cp, '\t'); if (cp == NULL) continue; *cp++ = '\0'; pathname=cp; cp = strchr (cp, '\t'); if (cp == NULL) continue; *cp++ = '\0'; watches = cp; cpo = cp; cp = strchr (cp, '\t'); if(cp) { *cp++ = '\0'; tag = cp; cp = strchr (cp, '\t'); if (cp == NULL) continue; *cp++ = '\0'; flags = cp; cp = strchr (cp, '\t'); if (cp == NULL) continue; *cp++ = '\0'; bugno = cp; cp = strchr (cp, '\t'); if (cp == NULL) continue; *cp++ = '\0'; msg = cp; } else { cp = cpo; tag = NULL; flags = NULL; bugno = NULL; msg = NULL; } cp = strchr (cp, '\n'); if (cp == NULL) continue; *cp = '\0'; if( notify_do (notif_type, filename, notify_user?notify_user:getcaller (), date,hostname,pathname, watches, finfo->repository, tag, flags, bugno&&*bugno?bugno:NULL, msg) ) { if(!rollback_list) rollback_list=getlist(); Node *n = findnode_fn(rollback_list,filename); if(!n) { n=getnode(); n->key=xstrdup(filename); addnode(rollback_list,n); } } } xfree (line); if (ferror (fp)) error (0, errno, "cannot read %s", CVSADM_NOTIFY); if (fclose (fp) < 0) error (0, errno, "cannot close %s", CVSADM_NOTIFY); if ( CVS_UNLINK (CVSADM_NOTIFY) < 0) error (0, errno, "cannot remove %s", CVSADM_NOTIFY); if(rollback_list) { Node *n = rollback_list->list->next; while(n!=rollback_list->list) { file_info fi; fi.file=n->key; fi.entries=finfo->entries; fi.update_dir=finfo->update_dir; unedit_fileproc(NULL,&fi); n=n->next; } dellist(&rollback_list); } return err; } /* Look through the CVSADM_NOTIFY file and process each item there accordingly. */ static int send_notifications (int argc, char **argv, int local) { int err = 0; /* OK, we've done everything which needs to happen on the client side. Now we can try to contact the server; if we fail, then the notifications stay in CVSADM_NOTIFY to be sent next time. */ if (current_parsed_root->isremote) { err += start_recursion (dummy_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, NULL); send_to_server ("noop\n", 0); if (strcmp (command_name, "release") == 0) err += get_server_responses (); else err += get_responses_and_close (); } else { /* Local. */ lock_tree_for_write (argc, argv, local, W_LOCAL, 0); err += start_recursion (ncheck_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, NULL); Lock_Cleanup (); } return err; } static int editors_output (struct file_info *finfo) { XmlHandle_t handle; const char *username, *hostname, *pathname, *time, *tag, *vtag, *bug; Vers_TS *vers; int out = 0; handle = fileattr_find(NULL,"file[@name=F'%s']/editor",finfo->file); if(!handle) return 0; vers = Version_TS(finfo,NULL,NULL,NULL,0,0,0); while (handle) { username=fileattr_getvalue(handle,"@name"); time=fileattr_getvalue(handle,"time"); hostname=fileattr_getvalue(handle,"hostname"); pathname=fileattr_getvalue(handle,"pathname"); tag=fileattr_getvalue(handle,"tag"); bug=fileattr_getvalue(handle,"bugid"); vtag = vers->tag; if(!vtag) vtag="HEAD"; if(all_editors || !tag || !strcmp(tag,vtag)) { if(!out) cvs_output (fn_root(finfo->fullname), 0); out=1; cvs_output ("\t", 1); if(username) cvs_output(username,0); cvs_output("\t", 1); if(time) cvs_output(time,0); cvs_output("\t", 1); if(hostname) cvs_output(hostname,0); cvs_output("\t", 1); if(pathname) cvs_output(fn_root(pathname),0); if(verbose) { cvs_output("\t", 1); if(bug) cvs_output(bug,0); } if(all_editors) { cvs_output("\t", 1); if(tag) cvs_output(tag,0); } cvs_output ("\n", 1); } handle = fileattr_next(handle); } freevers_ts(&vers); return 0; } /* check file that is to be edited if it's already being edited */ static int check_fileproc (void *callerdat, struct file_info *finfo) { XmlHandle_t handle; char *editors = NULL; int status; int errors = 0; Vers_TS *v; if (check_uptodate) { int q; q=really_quiet; really_quiet = 1; /* Supress any messages.. we just want status */ Ctype status = Classify_File (finfo, (char *) NULL, (char *) NULL, (char *) NULL, 1, 0, &v, 0, 0, 0); really_quiet = q; if ((status != T_UPTODATE) && (status != T_CHECKOUT) && (status != T_PATCH) && (status != T_REMOVE_ENTRY)) { error (0, 0, "%s is locally modified", fn_root(finfo->fullname)); freevers_ts (&v); return (1); } } else v = Version_TS (finfo, NULL, NULL, NULL, 0, 0, 0); kflag kftmp; RCS_get_kflags(v->options,false,kftmp); if(kftmp.flags&KFLAG_EXCLUSIVE_EDIT) exclusive_edit = 1; editors_found = 0; if (current_parsed_root->isremote) { int first_time; int len = 0; int possibly_more_editors = 0; char *argv = (char*)finfo->fullname; send_file_names (1, &argv, 0); if(supported_request("editors-edit")) send_to_server("editors-edit\n", 0); else send_to_server ("editors\n", 0); first_time = 1; do { possibly_more_editors = 0; to_server_buffer_flush (); from_server_buffer_read (&editors, &len); if (editors != NULL) { if (!strncmp(editors, "error ",6)) { errors = 1; if(editors[6]) { cvs_outerr(editors+6,0); cvs_outerr("\n",0); } possibly_more_editors = 0; } else if (strcmp (editors, "ok")) { possibly_more_editors = 1; switch (editors[0]) { case 'M': { if(fnncmp(editors+2,finfo->fullname,strchr(editors+2,'\t')-(editors+2))) break; kflag kftmp; RCS_get_kflags(v->options,false,kftmp); if(check_edited>=0 && (check_edited || kftmp.flags&KFLAG_RESERVED_EDIT)) editors_found = 2; else editors_found = 1; if(!really_quiet) { cvs_output (editors + 2, 0); cvs_output ("\n", 0); } break; } default: { struct response *rs = NULL; char *cmd = NULL; cmd = editors; for (rs = responses; rs->name != NULL; ++rs) if (strncmp (cmd, rs->name, strlen (rs->name)) == 0) { int cmdlen = strlen (rs->name); if (cmd[cmdlen] == ' ') ++cmdlen; else if (cmd[cmdlen] != '\0') /* * The first len characters match, but it's a different * response. e.g. the response is "oklahoma" but we * matched "ok". */ continue; (*rs->func) (cmd + cmdlen, len - cmdlen); break; } if (rs->name == NULL) /* It's OK to print just to the first '\0'. */ /* We might want to handle control characters and the like in some other way other than just sending them to stdout. One common reason for this error is if people use :ext: with a version of rsh which is doing CRLF translation or something, and so the client gets "ok^M" instead of "ok". Right now that will tend to print part of this error message over the other part of it. It seems like we could do better (either in general, by quoting or omitting all control characters, and/or specifically, by detecting the CRLF case and printing a specific error message). */ error (0, 0, "warning: unrecognized response `%s' from cvs server", cmd); break; } } } xfree(editors); } } while (!errors && possibly_more_editors); } else { /* This is a somewhat screwy way to check for this, because it doesn't help errors other than the nonexistence of the file (e.g. permissions problems). It might be better to rearrange the code so that CVSADM_NOTIFY gets written only after the various actions succeed (but what if only some of them succeed). */ if (!isfile (finfo->file)) { error (0, 0, "no such file %s; ignored", fn_root(finfo->fullname)); return 0; } handle = fileattr_find(NULL,"file[@name=F'%s']/editor",finfo->file); if(!really_quiet && handle != NULL) { editors_output (finfo); } if(handle != NULL) { kflag kftmp; RCS_get_kflags(v->options,false,kftmp); if(check_edited>=0 && (check_edited || kftmp.flags&KFLAG_RESERVED_EDIT)) editors_found = 2; else editors_found = 1; } } if(errors || editors_found==2) status = 1; else status = 0; freevers_ts(&v); return status; } /* Look through the CVS/fileattr file and check for editors */ static int check_edits (int argc, char **argv, int local) { int err = 0; if (current_parsed_root->isremote) { if (local) send_arg ("-l"); send_arg("--"); send_files (argc, argv, local, 0, SEND_NO_CONTENTS); } err += start_recursion (check_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, NULL/*verify_write*/); if (current_parsed_root->isremote) { send_to_server ("noop\n", 0); err += get_server_responses (); } return err; } static int edit_fileproc (void *callerdat, struct file_info *finfo) { FILE *fp; char *basefilename; char *oldfilename; Vers_TS *vers; const char *file = finfo->file; if (noexec) return 0; /* This is a somewhat screwy way to check for this, because it doesn't help errors other than the nonexistence of the file (e.g. permissions problems). It might be better to rearrange the code so that CVSADM_NOTIFY gets written only after the various actions succeed (but what if only some of them succeed). */ if (!isfile (finfo->file)) { error (0, 0, "no such file %s; ignored", fn_root(finfo->fullname)); return 0; } fp = open_file (CVSADM_NOTIFY, "a"); vers = Version_TS(finfo,NULL,NULL,NULL,0,0,0); if(vers->entdata && vers->entdata->user) file = vers->entdata->user; char *wd = (char *) xmalloc (strlen (CurDir) + strlen ("/") + strlen (finfo->update_dir) + 1); strcpy(wd, CurDir); if(finfo->update_dir != NULL && *finfo->update_dir != '\0') { strcat(wd, "/"); strcat(wd, finfo->update_dir); } fprintf (fp, "E%s\t%s GMT\t%s\t%s\t", file, global_session_time, hostname, wd); xfree(wd); if (setting_tedit) fprintf (fp, "E"); if (setting_tunedit) fprintf (fp, "U"); if (setting_tcommit) fprintf (fp, "C"); // convert carriage returns in the message to spaces // before writing to the Notify file char *message2=NULL; if (message) { message2=(char *)xmalloc(sizeof(char) * strlen(message)+1); if (message2!=NULL) { char *badpos; strcpy(message2,message); while ((badpos=strchr(message2,'\n'))!=NULL) *badpos=' '; } } fprintf(fp,"\t%s\t%s\t%s\t%s",whole_file?"":vers->tag?vers->tag:"HEAD",exclusive_edit?"X":"",bugid.size()?bugid.c_str():"",message2?message2:""); if (message2) xfree(message2); fprintf (fp, "\n"); if (fclose (fp) < 0) { if (finfo->update_dir[0] == '\0') error (0, errno, "cannot close %s", CVSADM_NOTIFY); else error (0, errno, "cannot close %s/%s", finfo->update_dir, CVSADM_NOTIFY); } #if defined(_WIN32) && !defined(CVS95) if(set_acl) win32_set_edit_acl(file); #endif xchmod (file, 1); /* Now stash the file away in CVSADM so that unedit can revert even if it can't communicate with the server. */ /* Could save a system call by only calling mkdir_if_needed if trying to create the output file fails. But copy_file isn't set up to facilitate that. */ mkdir_if_needed (CVSADM_BASE); basefilename = (char*)xmalloc (16 + sizeof CVSADM_BASE + strlen (file)); oldfilename = (char*)xmalloc (16 + sizeof CVSADM_BASE + strlen (file)); strcpy (basefilename, CVSADM_BASE); strcat (basefilename, "/"); strcat (basefilename, file); strcpy(oldfilename,basefilename); if(gzip_copies) { strcat (basefilename, ".gz"); copy_and_zip_file (file, basefilename, 1, 1); } else { strcat (oldfilename, ".gz"); copy_file (file, basefilename, 1, 1); } if(unlink_file(oldfilename) && !existence_error(errno)) error(1, errno, "unable to remove old %s", oldfilename); xchmod (basefilename, 0); xfree (basefilename); xfree (oldfilename); if (vers->vn_user) { Register(finfo->entries, file, vers->vn_user, vers->ts_rcs, vers->options, vers->tag, vers->date, vers->ts_conflict, vers->entdata->merge_from_tag_1, vers->entdata->merge_from_tag_2, vers->tt_rcs, vers->vn_user,whole_file||vers->tag?vers->tag:"HEAD",bugid.c_str()); } freevers_ts(&vers); return 0; } static const char *const edit_usage[] = { "Usage: %s %s [-cflRz] [files...]\n", #if defined(_WIN32) && !defined(CVS95) "\t-A\t\tSet ACL on edited file (experimental)\n", #endif "\t-a\t\tSpecify what actions for temporary watch, one of\n", "\t\t\t\tedit,unedit,commit,all,none\n", "\t-b bugid\tBug to associate with edit (repeat for multiple bugs)\n", "\t-c\t\tCheck that working files are unedited\n", "\t-C\t\tCheck that working files are up to date\n", "\t-f\t\tForce edit if working files are edited (default)\n", "\t-l\t\tLocal directory only, not recursive\n", "\t-m message\tSpecify reason for edit\n", "\t-R\t\tProcess directories recursively (default)\n", "\t-w\t\tLock whole file, not just this branch\n", "\t-x\t\tExclusive edit (Stop other users editing this file)\n", "\t-z\t\tCompress base revision copies\n", "(Specify the --help global option for a list of other help options)\n", NULL }; int edit (int argc, char **argv) { int local = 0; int c; int err = 0; int a_omitted; if (argc == -1) usage (edit_usage); a_omitted = 1; setting_tedit = 0; setting_tunedit = 0; setting_tcommit = 0; optind = 0; while ((c = getopt (argc, argv, "+cflRa:zb:m:xwAC")) != -1) { switch (c) { case 'c': check_edited = 1; break; case 'C': check_uptodate = 1; break; case 'f': check_edited = -1; break; case 'l': local = 1; break; case 'R': local = 0; break; case 'w': whole_file = 1; break; case 'A': #if defined(_WIN32) && !defined(CVS95) set_acl = 1; #endif break; case 'a': a_omitted = 0; if (strcmp (optarg, "edit") == 0) setting_tedit = 1; else if (strcmp (optarg, "unedit") == 0) setting_tunedit = 1; else if (strcmp (optarg, "commit") == 0) setting_tcommit = 1; else if (strcmp (optarg, "all") == 0) { setting_tedit = 1; setting_tunedit = 1; setting_tcommit = 1; } else if (strcmp (optarg, "none") == 0) { setting_tedit = 0; setting_tunedit = 0; setting_tcommit = 0; } else usage (edit_usage); break; case 'z': gzip_copies = 1; break; case 'b': if(bugid.size()) bugid+=","; bugid += optarg; break; case 'm': message = xstrdup(optarg); break; case 'x': exclusive_edit = 1; break; case '?': default: usage (edit_usage); break; } } argc -= optind; argv += optind; if (a_omitted) { setting_tedit = 1; setting_tunedit = 1; setting_tcommit = 1; } if (strpbrk (hostname, "+,>;=\t\n") != NULL) error (1, 0, "host name (%s) contains an invalid character (+,>;=\\t\\n)", hostname); if (strpbrk (CurDir, "+,>;=\t\n") != NULL) error (1, 0, "current directory (%s) contains an invalid character (+,>;=\\t\\n)", CurDir); #ifdef SERVER_SUPPORT if(current_parsed_root->isremote && supported_request("Error-If-Reader")) send_to_server("Error-If-Reader The 'cvs edit' command requires write access to the repository\n",0); #endif /* No need to readlock since we aren't doing anything to the repository. */ #ifdef SERVER_SUPPORT TRACE(3,"edit(server_active = %s, bugid = %s, bugid.size = %d)", (server_active)?"yes":"no", PATCH_NULL(bugid.c_str()), bugid.size()); if (server_active) { // this never gets called! if (bugid.size()==0) { char buffer2[MAX_PATH]="\0"; int bugsmandatory=0; if(!CGlobalSettings::GetGlobalValue("cvsnt","PServer","BugsMandatory",buffer2,sizeof(buffer2))) bugsmandatory = atoi(buffer2); if ((bugsmandatory==1)||(bugsmandatory==3)) error(1,0,"Bug number is required."); } } #endif err = check_edits (argc, argv, local); if(err && editors_found) error(1,0,"Files being edited!"); if(!err) { err = start_recursion (edit_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, NULL/*verify_write*/); err += send_notifications (argc, argv, local); } return err; } static int unedit_fileproc (void *callerdat, struct file_info *finfo) { FILE *fp; char *basefilename; char *gzipfilename; int gzip = 0; Node *node; Entnode *entdata; bool do_unedit=true; if (noexec) return 0; node = findnode_fn (finfo->entries, finfo->file); if(!node) { error(0,0,"? %s",finfo->file); return 0; /* No file */ } entdata = (Entnode *) node->data; if(bugid.size() && (!entdata->edit_bugid || !bugid_in(bugid.c_str(),entdata->edit_bugid))) return 0; basefilename = (char*)xmalloc (10 + sizeof CVSADM_BASE + strlen (entdata->user)); gzipfilename = (char*)xmalloc (10 + sizeof CVSADM_BASE + strlen (entdata->user)); strcpy (basefilename, CVSADM_BASE); strcat (basefilename, "/"); strcat (basefilename, entdata->user); strcpy (gzipfilename, basefilename); strcat (gzipfilename, ".gz"); if(isfile(gzipfilename)) { copy_and_unzip_file(gzipfilename,basefilename, 1, 1); if(revert_only) gzip = 1; else { if(unlink_file(gzipfilename) && !existence_error(errno)) error(1, errno, "Unable to remove gzip copy %s", gzipfilename); } } xfree(gzipfilename); if(isfile(basefilename)) { if (!force_unedit && isfile(entdata->user) && xcmp (entdata->user, basefilename) != 0) { char *tmp=(char*)xmalloc(strlen(fn_root(finfo->fullname))+sizeof(" has been modified; revert changes? ")+100); sprintf(tmp,"%s has been modified; revert changes? ",fn_root(finfo->fullname)); if (yesno_prompt(tmp,"Modified file",0)!='y') { /* "no". */ xfree (basefilename); xfree(tmp); return 0; } xfree(tmp); } if(revert_only && !gzip) copy_file(basefilename, entdata->user,1,1); else rename_file (basefilename, entdata->user); } else do_unedit=false; xfree (basefilename); if(!revert_only) { fp = open_file (CVSADM_NOTIFY, "a"); fprintf (fp, "U%s\t%s GMT\t%s\t%s\t\t%s\t\t%s\t%s\n", entdata->user, global_session_time, hostname, CurDir, entdata->edit_tag?entdata->edit_tag:"", entdata->edit_bugid?entdata->edit_bugid:"", message?message:""); if (fclose (fp) < 0) { if (finfo->update_dir[0] == '\0') error (0, errno, "cannot close %s", CVSADM_NOTIFY); else error (0, errno, "cannot close %s/%s", finfo->update_dir, CVSADM_NOTIFY); } } cvs::filename fn = entdata->user; if(isfile(fn.c_str())) { /* Now update the revision number in CVS/Entries from CVS/Baserev. The basic idea here is that we are reverting to the revision that the user edited. If we wanted "cvs update" to update CVS/Base as we go along (so that an unedit could revert to the current repository revision), we would need: update (or all send_files?) (client) needs to send revision in new Entry-base request. update (server/local) needs to check revision against repository and send new Update-base response (like Update-existing in that the file already exists. While we are at it, might try to clean up the syntax by having the mode only in a "Mode" response, not in the Update-base itself). */ if(do_unedit) { Register (finfo->entries, entdata->user, (entdata->edit_revision&&*entdata->edit_revision)?entdata->edit_revision:entdata->version, entdata->timestamp, entdata->options, (entdata->edit_tag&&*entdata->edit_tag)?(strcmp(entdata->edit_tag,"HEAD")?entdata->edit_tag:NULL):entdata->tag, entdata->date, entdata->conflict, NULL, NULL, entdata->rcs_timestamp, revert_only?entdata->edit_revision:NULL, revert_only?entdata->edit_tag:NULL, revert_only?entdata->edit_revision:NULL); } else { /* No unedit was done... remove the (bogus) edit information only */ Register (finfo->entries, entdata->user, entdata->version, entdata->timestamp, entdata->options, entdata->tag, entdata->date, entdata->conflict, NULL, NULL, entdata->rcs_timestamp, NULL, NULL, NULL); } } /* Note entdata is most likely invalid at this point as Register() changes the entry list */ xchmod (fn.c_str(), keep_write); return 0; } static const char *const unedit_usage[] = { "Usage: %s %s [-lRwy] [-r] [-u user] [-b bug] [-m message] [files...]\n", "\t-b \tUnedit only files related to bug\n", "\t-l\t\tLocal directory only, not recursive\n", "\t-m \tSpecify reason for unedit\n", "\t-r\t\tRevert file only, don't unedit\n", "\t-R\t\tProcess directories recursively\n", "\t-u \tUnedit other user (repository administrators only)\n", "\t-w\t\tLeave file writable after unedit\n", "\t-y\t\tForce unedit of modified file\n", "(Specify the --help global option for a list of other help options)\n", NULL }; int unedit (int argc, char **argv) { int local = 0; int c; int err; if (argc == -1) usage (unedit_usage); optind = 0; while ((c = getopt (argc, argv, "+lRu:wrb:m:y")) != -1) { switch (c) { case 'l': local = 1; break; case 'R': local = 0; break; case 'u': if(notify_user) error(1,0,"Can only specify -u once."); if(server_active && !supported_request("NotifyUser")) error(1,0,"Remote server does not support unediting other users"); notify_user = xstrdup(optarg); local = 1; break; case 'w': keep_write = 1; break; case 'r': revert_only = 1; break; case 'b': if(!RCS_check_bugid(optarg)) error(1,0,"Invalid characters in bug identifier. Please avoid ,\"'"); if(bugid.size()) bugid+=","; bugid +=optarg; break; case 'y': force_unedit = 1; break; case 'm': message = xstrdup(optarg); break; case '?': default: usage (unedit_usage); break; } } argc -= optind; argv += optind; /* No need to readlock since we aren't doing anything to the repository. */ err = start_recursion (unedit_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 0, (char *)NULL, NULL, 0, NULL/*verify_write*/); err += send_notifications (argc, argv, local); xfree(notify_user); return err; } void mark_up_to_date (const char *file) { char *base; if(commit_keep_edits) return; /* The file is up to date, so we better get rid of an out of date file in CVSADM_BASE. */ base = (char*)xmalloc (strlen (file) + 80); strcpy (base, CVSADM_BASE); strcat (base, "/"); strcat (base, file); if (unlink_file (base) < 0 && ! existence_error (errno)) error (0, errno, "cannot remove %s", file); strcat(base,".gz"); if (unlink_file (base) < 0 && ! existence_error (errno)) error (0, errno, "cannot remove %s", file); xfree (base); } static void editor_set (char type, const char *filename, const char *editor, const char *time, const char *hostname, const char *pathname, const char *tag, const char *flags, const char *bugid, const char *message, const char *repository) { XmlHandle_t handle,ehandle; TRACE(2,"editor_set(%c,%s,%s,%s,%s,%s)",type,PATCH_NULL(filename),PATCH_NULL(editor),PATCH_NULL(time),PATCH_NULL(hostname),PATCH_NULL(pathname)); if(type=='C' || type=='U') { handle = fileattr_find(NULL,"file[@name=F'%s']",filename); if(handle) { while((ehandle=fileattr_find(handle,"editor[@name=U'%s']",editor))!=NULL) fileattr_delete_child(handle,ehandle); fileattr_prune(handle); } history_write('u',pathname,tag,filename,repository,bugid,message); } else { handle = fileattr_create(NULL,"file[@name=F'%s']/editor[@name=U'%s']",filename,editor); if(!handle) { TRACE(3,"fileattr_create failed"); return; } if(tag && *tag) fileattr_setvalue(handle,"tag",tag); fileattr_setvalue(handle,"time",time); fileattr_setvalue(handle,"hostname",hostname); fileattr_setvalue(handle,"pathname",fn_root(pathname)); if(bugid && *bugid) fileattr_setvalue(handle,"bugid",bugid); if(message && *message) fileattr_setvalue(handle,"message",message); if(flags && flags[0]=='X') fileattr_setvalue(handle,"exclusive",NULL); history_write('e',pathname,tag,filename,repository,bugid,message); } } struct notify_proc_args { /* What kind of notification, "edit", "tedit", etc. */ const char *type; /* User who is running the command which causes notification. */ const char *who; /* Time of notification */ const char *date; /* Tag/Branch being edited */ const char *tag; /* Bug id */ const char *bugid; /* Unedit message */ const char *message; /* User to be notified. */ const char *notifyee; /* File. */ const char *file; /* Repository */ const char *repository; }; static int notify_proc(void *params, const trigger_interface *cb) { notify_proc_args *args = (notify_proc_args *)params; int ret = 0; if(cb->notify) { const char *srepos; srepos = Short_Repository (args->repository); ret = cb->notify(cb,args->message,args->bugid,srepos,args->notifyee,args->tag,args->type?args->type:"",args->file); } return ret; } int check_can_edit(const char *repository, const char *filename, const char *who, const char *tag) { if(!verify_write(repository,filename,tag,NULL,NULL)) return 1; XmlHandle_t handle = fileattr_find(NULL,"file[@name=F'%s']/editor",filename); if(!handle) return 0; while(handle) { const char *username=fileattr_getvalue(handle,"@name"); const char *edittag = fileattr_getvalue(handle,"tag"); int exclusive = fileattr_find(handle,"exclusive")==NULL?0:1; if(exclusive) { if(!edittag) return 1; /* Exclusive lock on whole file */ if(!tag) return 1; /* Someone else has an exclusive lock, we can't get a file lock */ if(!strcmp(edittag,tag) && usercmp(who,username)) return 1; /* Someone else has an exclusive lock on this branch */ } handle = fileattr_next(handle); } return 0; } int notify_do (int type, const char *filename, const char *who, const char *date, const char *hostname, const char *pathname, const char *watches, const char *repository, const char *tag, const char *flags, const char *bugid, const char *message) { XmlHandle_t filehandle; struct addremove_args args = {0}; TRACE(3,"notify_do (%c, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", type, PATCH_NULL(filename), PATCH_NULL(who), PATCH_NULL(date), PATCH_NULL(hostname), PATCH_NULL(pathname), PATCH_NULL(watches), PATCH_NULL(repository), PATCH_NULL(tag), PATCH_NULL(flags), PATCH_NULL(bugid), PATCH_NULL(message)); switch (type) { case 'E': if(check_can_edit(repository,filename,who,tag)) { error(0,0,"Edit on file '%s' refused by server", filename); return 1; } editor_set (type, filename, who, date,hostname,pathname,tag,flags,bugid,message,repository); break; case 'U': case 'C': editor_set (type, filename, who, date,hostname,pathname,tag,flags,bugid,message,repository); break; default: error(0,0,"Unknown notify '%c' ignored",type); return 0; } filehandle = fileattr_find(NULL,"file[@name=F'%s']/watcher",filename); while(filehandle) { const char *username = fileattr_getvalue(filehandle,"@name"); const char *notif = NULL; /* Don't notify user of their own changes. Would perhaps be better to check whether it is the same working directory, not the same user, but that is hairy. */ if(username && usercmp(username,CVS_Username)) { if (type == 'E' && fileattr_find(filehandle,"edit")) notif = "edit"; else if(type == 'U' && fileattr_find(filehandle,"unedit")) notif = "unedit"; else if (type == 'C' && fileattr_find(filehandle,"commit")) notif = "commit"; else if (type == 'E' && fileattr_find(filehandle,"temp_edit")) notif = "temporary edit"; else if(type == 'U' && fileattr_find(filehandle,"temp_unedit")) notif = "temporary unedit"; else if (type == 'C' && fileattr_find(filehandle,"temp_commit")) notif = "temporary commit"; } if (notif != NULL) { struct notify_proc_args args; size_t user_len = strlen(username); FILE *fp; char *usersname; char *line = NULL; size_t line_len = 0; args.notifyee = NULL; usersname = (char*)xmalloc (strlen (current_parsed_root->directory) + sizeof CVSROOTADM + sizeof CVSROOTADM_USERS + 20); strcpy (usersname, current_parsed_root->directory); strcat (usersname, "/"); strcat (usersname, CVSROOTADM); strcat (usersname, "/"); strcat (usersname, CVSROOTADM_USERS); fp = CVS_FOPEN (usersname, "r"); if (fp == NULL && !existence_error (errno)) error (0, errno, "cannot read %s", usersname); if (fp != NULL) { while (getline (&line, &line_len, fp) >= 0) { if (strncmp (line, username, user_len) == 0 && line[user_len] == ':') { char *cp; args.notifyee = xstrdup (line + user_len + 1); /* There may or may not be more colon-separated fields added to this in the future; in any case, we ignore them right now, and if there are none we make sure to chop off the final newline, if any. */ cp = strpbrk (args.notifyee, ":\n"); if (cp != NULL) *cp = '\0'; break; } } if (ferror (fp)) error (0, errno, "cannot read %s", usersname); if (fclose (fp) < 0) error (0, errno, "cannot close %s", usersname); } xfree (usersname); if (line != NULL) xfree (line); if (args.notifyee == NULL) args.notifyee = xstrdup(username); args.type = notif; args.who = who; args.file = filename; args.date = date?date:""; args.bugid = bugid?bugid:""; args.message = message?message:""; args.tag = tag?tag:""; args.repository = repository?repository:""; TRACE(3,"run notify trigger 1"); run_trigger(&args,notify_proc); xfree (args.notifyee); } filehandle = fileattr_next(filehandle); } if(global_watcher) { struct notify_proc_args args; const char *notif; if (type == 'E') notif = "edit"; else if(type == 'U') notif = "unedit"; else if (type == 'C') notif = "commit"; args.notifyee = global_watcher; args.type = notif; args.who = who; args.file = filename; args.date = date?date:""; args.bugid = bugid?bugid:""; args.message = message?message:""; args.tag = tag?tag:""; args.repository = repository?repository:""; TRACE(3,"run notify trigger 2"); run_trigger(&args, notify_proc); } switch (type) { case 'E': args.add_tedit = 1; args.add_tunedit = 1; args.add_tcommit = 1; args.adding=1; watch_modify_watchers (filename, who, &args); break; case 'U': case 'C': args.remove_temp = 1; args.adding=0; watch_modify_watchers (filename, who, &args); break; } return 0; } /* Check and send notifications. This is only for the client. */ void client_notify_check (char *repository, char *update_dir) { FILE *fp; char *line = NULL; size_t line_len = 0; if (! server_started) /* We are in the midst of a command which is not to talk to the server (e.g. the first phase of a cvs edit). Just chill out, we'll catch the notifications on the flip side. */ return; /* We send notifications even if noexec. I'm not sure which behavior is most sensible. */ fp = CVS_FOPEN (CVSADM_NOTIFY, "r"); if (fp == NULL) { if (!existence_error (errno)) error (0, errno, "cannot open %s", CVSADM_NOTIFY); return; } while (getline (&line, &line_len, fp) > 0) { int notif_type; char *filename; char *val; char *cp; notif_type = line[0]; if (notif_type == '\0') continue; filename = line + 1; cp = strchr (filename, '\t'); if (cp == NULL) continue; *cp++ = '\0'; val = cp; client_notify (repository, update_dir, filename, notif_type, val, notify_user); } if (line) xfree (line); if (ferror (fp)) error (0, errno, "cannot read %s", CVSADM_NOTIFY); if (fclose (fp) < 0) error (0, errno, "cannot close %s", CVSADM_NOTIFY); /* Leave the CVSADM_NOTIFY file there, until the server tells us it has dealt with it. */ } static const char *const editors_usage[] = { "Usage: %s %s [-aclRv] [files...]\n", "\t-a\tShow all branches.\n", "\t-c\tCheck whether edit is valid on file.\n", "\t-l\tProcess this directory only (not recursive).\n", "\t-R\tProcess directories recursively.\n", "\t-v\tShow bugs.\n", "(Specify the --help global option for a list of other help options)\n", NULL }; static int editors_fileproc (void *callerdat, struct file_info *finfo) { if(check_edit) { Node *n = findnode_fn(finfo->entries,finfo->file); if(n && n->data) { Entnode *ent = (Entnode*)n->data; if(check_can_edit(finfo->repository,finfo->file,CVS_Username,ent->tag)) error(1,0,"Edit on file '%s' refused by server", finfo->file); } } return editors_output (finfo); } int editors (int argc, char **argv) { int local = 0; int c; if (argc == -1) usage (editors_usage); if(!strcmp(command_name,"editors-edit")) check_edit = 1; optind = 0; while ((c = getopt (argc, argv, "+lRavc")) != -1) { switch (c) { case 'a': all_editors = 1; break; case 'v': verbose = 1; break; case 'l': local = 1; break; case 'R': local = 0; break; case 'c': check_edit = 1; break; case '?': default: usage (editors_usage); break; } } argc -= optind; argv += optind; #ifdef SERVER_SUPPORT TRACE(3,"editors(server_active = %s)", (server_active)?"yes":"no"); #endif if (current_parsed_root->isremote) { if (local) send_arg ("-l"); if(all_editors) send_arg ("-a"); send_arg("--"); send_files (argc, argv, local, 0, SEND_NO_CONTENTS); send_file_names (argc, argv, SEND_EXPAND_WILD); send_to_server ("editors\n", 0); return get_responses_and_close (); } return start_recursion (editors_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, (DIRENTPROC) NULL, (DIRLEAVEPROC) NULL, NULL, argc, argv, local, W_LOCAL, 0, 1, (char *)NULL, NULL, 0, verify_read); }